Explorar el Código

[search] Simpliying search API

Romain Rigaux hace 11 años
padre
commit
fe88c7d

+ 36 - 0
apps/search/src/search/api.py

@@ -91,6 +91,42 @@ class SolrApi(object):
     except RestException, e:
       raise PopupException(e, title=_('Error while accessing Solr'))
 
+  #@demo_handler
+  def query2(self, solr_query, dd):
+
+      params = self._get_params() + (
+          ('q', solr_query['q'] or EMPTY_QUERY.get()),
+          ('wt', 'json'),
+          ('rows', solr_query['rows']),
+          ('start', solr_query['start']),
+      )
+
+      #params += hue_core.get_query(solr_query)
+      if dd:
+        params += (
+          ('facet', 'true'),
+          ('facet.mincount', 0),
+          ('facet.limit', 10),
+          ##('facet.sort', properties.get('sort')),
+        )
+        # {!ex=dt}
+        params += tuple([('facet.field', '{!ex=%s}%s' % (d['field'], d['field'])) for d in dd])
+
+
+      fqs = solr_query['fq'] #.split('|')
+      print 'fq'
+      print fqs
+      for fq, val in fqs.iteritems():
+        params += (('fq', urllib.unquote(utf_quoter('{!tag=%s}%s:%s' % (fq, fq, val)))),)
+
+      if solr_query.get('fl'):
+        params += (('fl', urllib.unquote(utf_quoter(','.join(solr_query['fl'])))),)
+
+      response = self._root.get('%(collection)s/select' % solr_query, params)
+
+      return self._get_json(response)
+
+
   def suggest(self, solr_query, hue_core):
     try:
       params = self._get_params() + (

+ 2 - 2
apps/search/src/search/forms.py

@@ -28,7 +28,7 @@ class QueryForm(forms.Form):
   collection = forms.ChoiceField() # Aka collection_id
 
   query = forms.CharField(label='', max_length=256, required=False, initial='',
-                          widget=forms.TextInput(attrs={'class': 'search-query input-xxlarge'}))
+                          widget=forms.TextInput(attrs={'class': 'search-query input-xxlarge', 'data-bind': 'value: q'}))
   fq = forms.CharField(label='', max_length=256, required=False, initial='', widget=forms.HiddenInput(), help_text='Solr Filter query')
   sort = forms.CharField(label='', max_length=256, required=False, initial='', widget=forms.HiddenInput(), help_text='Solr sort')
   rows = forms.CharField(label='', required=False, initial='', widget=forms.HiddenInput(), help_text='Solr records per page')
@@ -93,4 +93,4 @@ class CollectionForm(forms.ModelForm):
     name = self.cleaned_data['name']
     if not searcher.is_collection(name) and not searcher.is_core(name):
       raise forms.ValidationError(_('No live Solr collection or core by the name %s.') % name)
-    return name
+    return name

+ 136 - 23
apps/search/src/search/models.py

@@ -15,7 +15,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
+import itertools
 import json
 import logging
 import re
@@ -62,9 +62,9 @@ class Facet(models.Model):
       ('facet.sort', properties.get('sort')),
     )
 
-    if data_dict.get('fields'):
-      field_facets = tuple([('facet.field', field_facet['field']) for field_facet in data_dict['fields']])
-      params += field_facets
+#    if data_dict.get('fields'):
+#      field_facets = tuple([('facet.field', field_facet['field']) for field_facet in data_dict['fields']])
+#      params += field_facets
 
     if data_dict.get('charts'):
       for field_facet in data_dict['charts']:
@@ -346,24 +346,28 @@ def get_facet_field_format(field, type, facets):
     pass
   return format
 
-def get_facet_field_label(field, type, facets):
+def get_facet_field_label(field, facets):
   label = field
-  if type == 'field':
-    for fld in facets['fields']:
-      if fld['field'] == field:
-        label = fld['label']
-  elif type == 'range':
-    for fld in facets['ranges']:
-      if fld['field'] == field:
-        label = fld['label']
-  elif type == 'date':
-    for fld in facets['dates']:
-      if fld['field'] == field:
-        label = fld['label']
-  elif type == 'chart':
-    for fld in facets['charts']:
-      if fld['field'] == field:
-        label = fld['label']
+
+  facets = filter(lambda facet: facet['type'] == 'field' and facet['field'] == field, facets)
+  if facets:
+    return facets[0]['label']
+#  if type == 'field':
+#    for fld in facets['fields']:
+#      if fld['field'] == field:
+#        label = fld['label']
+#  elif type == 'range':
+#    for fld in facets['ranges']:
+#      if fld['field'] == field:
+#        label = fld['label']
+#  elif type == 'date':
+#    for fld in facets['dates']:
+#      if fld['field'] == field:
+#        label = fld['label']
+#  elif type == 'chart':
+#    for fld in facets['charts']:
+#      if fld['field'] == field:
+#        label = fld['label']
   return label
 
 def get_facet_field_uuid(field, type, facets):
@@ -390,7 +394,100 @@ def is_chart_field(field, charts):
   return found
 
 
-def augment_solr_response(response, facets):
+def augment_solr_response2(response, facets, solr_query):
+  augmented = response
+  augmented['normalized_facets'] = []
+
+  normalized_facets = {}
+  default_facets = []
+
+#  chart_facets = facets.get('charts', [])
+
+  def pairwise(iterable):
+      "s -> (s0,s1), (s1,s2), (s2, s3), ..."
+      a, b = itertools.tee(iterable)
+      next(b, None)
+      return list(itertools.izip(a, b))
+
+  def pairwise2(cat, fq, iterable):
+      pairs = []
+      a, b = itertools.tee(iterable)
+      for element in a:
+        pairs.append({'cat': cat, 'value': element, 'count': next(a), 'selected': element == selected_field})
+      return pairs
+
+  fq = solr_query['fq']
+
+  if response and response.get('facet_counts'):
+    if response['facet_counts']['facet_fields']:
+      for cat in response['facet_counts']['facet_fields']:
+        selected_field = fq.get(cat, '')
+        facet = {
+          'field': cat,
+          'type': 'field',
+          'label': get_facet_field_label(cat, facets),
+          'counts': pairwise2(cat, selected_field, response['facet_counts']['facet_fields'][cat]),
+        }
+        uuid = '' #get_facet_field_uuid(cat, 'field', facets)
+        if uuid == '':
+          default_facets.append(facet)
+        else:
+          normalized_facets[uuid] = facet
+
+#    if response['facet_counts']['facet_ranges']:
+#      for cat in response['facet_counts']['facet_ranges']:
+#        facet = {
+#          'field': cat,
+#          'type': 'chart' if is_chart_field(cat, chart_facets) else 'range',
+#          'label': get_facet_field_label(cat, 'range', facets),
+#          'counts': response['facet_counts']['facet_ranges'][cat]['counts'],
+#          'start': response['facet_counts']['facet_ranges'][cat]['start'],
+#          'end': response['facet_counts']['facet_ranges'][cat]['end'],
+#          'gap': response['facet_counts']['facet_ranges'][cat]['gap'],
+#        }
+#        uuid = get_facet_field_uuid(cat, 'range', facets)
+#        if uuid == '':
+#          default_facets.append(facet)
+#        else:
+#          normalized_facets[uuid] = facet
+#
+#    if response['facet_counts']['facet_dates']:
+#      for cat in response['facet_counts']['facet_dates']:
+#        facet = {
+#          'field': cat,
+#          'type': 'date',
+#          'label': get_facet_field_label(cat, 'date', facets),
+#          'format': get_facet_field_format(cat, 'date', facets),
+#          'start': response['facet_counts']['facet_dates'][cat]['start'],
+#          'end': response['facet_counts']['facet_dates'][cat]['end'],
+#          'gap': response['facet_counts']['facet_dates'][cat]['gap'],
+#        }
+#        counts = []
+#        for date, count in response['facet_counts']['facet_dates'][cat].iteritems():
+#          if date not in ('start', 'end', 'gap'):
+#            counts.append(date)
+#            counts.append(count)
+#        facet['counts'] = counts
+#
+#        uuid = get_facet_field_uuid(cat, 'date', facets)
+#        if uuid == '':
+#          default_facets.append(facet)
+#        else:
+#          normalized_facets[uuid] = facet
+
+#  for ordered_uuid in facets.get('order', []):
+#    try:
+#      augmented['normalized_facets'].append(normalized_facets[ordered_uuid])
+#    except:
+#      pass
+
+  if default_facets:
+    augmented['normalized_facets'].extend(default_facets)
+
+  return augmented
+
+
+def augment_solr_response(response, facets, solr_query):
   augmented = response
   augmented['normalized_facets'] = []
 
@@ -399,14 +496,30 @@ def augment_solr_response(response, facets):
 
   chart_facets = facets.get('charts', [])
 
+  def pairwise(iterable):
+      "s -> (s0,s1), (s1,s2), (s2, s3), ..."
+      a, b = itertools.tee(iterable)
+      next(b, None)
+      return list(itertools.izip(a, b))
+
+  def pairwise2(cat, fq, iterable):
+      pairs = []
+      a, b = itertools.tee(iterable)
+      for element in a:
+        pairs.append({'cat': cat, 'value': element, 'count': next(a), 'selected': element == selected_field})
+      return pairs
+
+  fq = solr_query['fq']
+
   if response and response.get('facet_counts'):
     if response['facet_counts']['facet_fields']:
       for cat in response['facet_counts']['facet_fields']:
+        selected_field = fq.get(cat, '')
         facet = {
           'field': cat,
           'type': 'chart' if is_chart_field(cat, chart_facets) else 'field',
           'label': get_facet_field_label(cat, is_chart_field(cat, chart_facets) and 'chart' or 'field', facets),
-          'counts': response['facet_counts']['facet_fields'][cat],
+          'counts': pairwise2(cat, selected_field, response['facet_counts']['facet_fields'][cat]),
         }
         uuid = get_facet_field_uuid(cat, 'field', facets)
         if uuid == '':

+ 285 - 0
apps/search/src/search/templates/search2.mako

@@ -0,0 +1,285 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##     http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+
+<%!
+from desktop.views import commonheader, commonfooter
+from django.utils.translation import ugettext as _
+from django.utils.dateparse import parse_datetime
+from search.api import utf_quoter
+import urllib
+import math
+import time
+%>
+
+<%namespace name="macros" file="macros.mako" />
+
+${ commonheader(_('Search'), "search", user, "90px") | n,unicode }
+
+<link rel="stylesheet" href="/search/static/css/search.css">
+<link href="/static/ext/css/hue-filetypes.css" rel="stylesheet">
+<script src="/static/ext/js/moment.min.js" type="text/javascript" charset="utf-8"></script>
+<script src="/search/static/js/search.utils.js" type="text/javascript" charset="utf-8"></script>
+<script src="/search/static/js/template.ko.js" type="text/javascript" charset="utf-8"></script>
+<script src="/search/static/js/query.ko.js" type="text/javascript" charset="utf-8"></script>
+
+
+<div class="search-bar">
+  % if user.is_superuser:
+    <div class="pull-right" style="margin-top: 6px; margin-right: 40px">
+      <a class="change-settings" href="#"><i class="fa fa-save"></i> ${ _('Save') }</a> &nbsp;&nbsp;
+      <a href="${ url('search:admin_collections') }"><i class="fa fa-sitemap"></i> ${ _('Collection manager') }</a>
+    </div>
+  % endif
+  
+  <form class="form-search" style="margin: 0" data-bind="submit: search">
+    <strong>${_("Search")}</strong>
+    <div class="input-append">
+      <div class="selectMask">
+        <span class="current-collection"></span>
+        <ul class="unstyled">
+          % if user.is_superuser:
+            <li><a class="dropdown-collection" href="#" data-value="${ hue_collection.id }" data-settings-url="${ hue_collection.get_absolute_url() }">${ hue_collection.label }</a></li>
+          % else:
+            <li><a class="dropdown-hue_collection" href="#" data-value="${ hue_collection.id }">${ hue_collection.label }</a></li>
+          % endif
+        </ul>
+      </div>
+
+      ${ search_form | n,unicode }
+      
+      <button type="submit" id="search-btn" class="btn btn-inverse"><i class="fa fa-search"></i></button>
+    </div>
+  </form>
+</div>
+
+    <div>
+      <ul class="nav nav-tabs">
+        <li class="active"><a href="#tourStep1" class="tourStep">${ _('Search') }</a></li>
+        <li><a href="#tourStep2" class="tourStep">${ _('Visualization') }</a></li>
+        <li><a id="tourLastStep" href="#tourStep3" class="tourStep">${ _('Data') }</a></li>
+      </ul>
+    </div>
+    
+    <div class="tourSteps">
+      <div id="tourStep1" class="tourStepDetails">
+
+<div class="container results">
+
+  <div id="mainContent" class="row">
+    <div class="span2 results">
+      <div class="row">
+        <span class="pull-right">
+          <a id="showAddFacetModal" href="javascript:void(0)"><i class="fa fa-plus"></i></a>
+        </span>
+      </div>
+      <div class="row" data-bind="foreach: norm_facets">
+        <span class="pull-right">
+          <a href="javascript:void(0)" data-bind="click: editFacet"><i class="fa fa-pencil"></i></a>
+          <a href="javascript:void(0)" data-bind="click: $root.removeFacet"><i class="fa fa-times"></i></a>
+          <i class="fa fa-arrows" id="move-facet"></i>
+        </span>
+        <div data-bind="text: label"></div>
+        <div data-bind="foreach: counts">
+          <div>
+            <a href="script:void(0)">
+            <!-- ko if: selected -->
+              <span data-bind="text: value, click: $root.unselectFacet"></span>
+              <i data-bind="click: $root.unselectFacet" class="fa fa-times"></i>            
+            <!-- /ko -->
+            <!-- ko if: !selected -->           
+              <span data-bind="text: value, click: $root.selectFacet"></span> (<span data-bind="text: count, click: $root.selectFacet"></span>)            
+            <!-- /ko -->
+            </a>
+          </div>
+        </div>
+      </div>            
+      
+    </div> 
+    
+    <div class="span10 results">
+      <div class="row">
+        <span class="pull-right">
+          <i class="fa fa-plus" id="edit-timeline"></i>
+        </span>
+      </div>
+    
+      <div class="row">
+        <span class="pull-right">
+          <a id="edit-template" href="javascript:void(0)"><i class="fa fa-pencil"></i></a>
+        </span>
+      </div>
+      
+      <!-- ko if: $root.template.isGridLayout() -->
+      <table id="result-container">        
+        <thead>
+          <tr data-bind="foreach: $root.template.fields">
+            <th data-bind="text: $data"></th>
+          </tr>
+        </thead>
+        <tbody data-bind="foreach: results">
+          <tr class="result-row" data-bind="foreach: $data">
+            <td data-bind="text: $data"></td>
+          </tr>
+        </tbody>
+      </table>
+      <!-- /ko -->
+      <!-- ko if: !$root.template.isGridLayout() -->
+      <div id="result-container" data-bind="foreach: results">
+        <div class="result-row" data-bind="html: $data"></div>
+      </div>
+      <!-- /ko -->
+      
+      </div>
+    </div>
+
+  </div>
+</div>
+        </div>
+      </div>      
+
+
+## unsused
+<script type="text/html" id="error-template">
+<div class="container-fluid">
+  <div class="row-fluid">
+    <div class="span12">
+      <div class="alert alert-error">
+        <h4 data-bind="text: title"></h4>
+        <br/>
+        <span class="decodeError" data-bind="text: message"></span>
+      </div>
+    </div>
+  </div>
+</div>
+</script>
+
+
+<div id="addFacetModal" class="modal hide fade">
+  <div class="modal-header">
+    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+    <h3>${_('Add facet')}</h3>
+  </div>
+  <div class="modal-body">
+    <p>
+      <div class="clearfix"></div>
+      <div style="margin-top: 20px">
+        <div class="input-append">
+          <input id="facetName" type="text">
+        </div>
+      </div>
+    </p>
+  </div>
+  <div class="modal-footer">
+    <a href="#" data-dismiss="modal" class="btn">${_('Back')}</a>
+    <a id="submitAddFacetModal" class="btn btn-primary disable-feedback">${_('Ok')}</a>
+  </div>
+</div>
+
+
+<div id="editFacetModal" class="modal hide fade">
+  <div class="modal-header">
+    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+    <h3>${_('Edit Facet')}</h3>
+  </div>
+  <div class="modal-body">
+    <p>
+      <div class="clearfix"></div>
+      <div style="margin-top: 20px">        
+        <!-- ko if: selectedFacet() -->
+        <p>
+          ${ _('Label') }: <input type="text" data-bind="value: selectedFacet().label" />
+        </p>
+        <!-- /ko -->
+      </div>
+    </p>
+  </div>
+  <div class="modal-footer">
+    <a href="#" data-dismiss="modal" class="btn" data-bind="click: search">${_('Ok')}</a>
+  </div>
+</div>
+
+
+<div id="editTemplateModal" class="modal hide fade">
+  <div class="modal-header">
+    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+    <h3>${_('Edit Template')}</h3>
+  </div>
+  <div class="modal-body">
+    <p>
+      <div class="clearfix"></div>
+      <div style="margin-top: 20px">        
+        <p>
+          ${ _('Grid result') }: <input type="checkbox" data-bind="checked: template.isGridLayout" />
+        </p>
+        
+        <!-- ko if: template.isGridLayout() -->
+        <p>
+          ${ _('Fields') }
+          <select data-bind="options: fields, selectedOptions: template.fields" size="5" multiple="true"></select>
+        </p>  
+        <!-- /ko -->
+        
+        <!-- ko if: !template.isGridLayout() -->
+        <textarea data-bind="value: template.template, valueUpdate:'afterkeydown'"></textarea>
+        <!-- /ko -->
+      </div>
+    </p>
+  </div>
+  <div class="modal-footer">
+    <a href="#" data-dismiss="modal" class="btn">${_('Ok')}</a>
+  </div>
+</div>
+
+##<script src="/search/static/js/query.ko.js" type="text/javascript" charset="utf-8"></script>
+
+<script src="/static/ext/js/knockout-min.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/js/knockout.mapping-2.3.2.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/js/mustache.js"></script>
+<script src="/search/static/js/template.ko.js" type="text/javascript" charset="utf-8"></script>
+
+<script type="text/javascript" charset="utf-8">
+var viewModel;
+
+$(document).ready(function () {
+  viewModel = new SearchViewModel(${ hue_collection.result.data | n,unicode }, ${ hue_collection.facets.data | n,unicode });
+  ko.applyBindings(viewModel);
+  
+  viewModel.search();
+
+  $("#showAddFacetModal").click(function() {
+    $("#addFacetModal").modal("show");
+  });
+
+  $("#submitAddFacetModal").click(function() {
+    viewModel.addFacet({'name': $("#facetName").val()});
+    $('#addFacetModal').modal("hide");
+    viewModel.search();
+  });
+
+  $("#edit-template").click(function() {
+    $("#editTemplateModal").modal("show");
+  });
+  
+
+});
+
+  function editFacet(facet) {
+    viewModel.selectSingleFacet(facet);
+    $("#editFacetModal").modal("show");
+  };
+</script>
+
+${ commonfooter(messages) | n,unicode }

+ 2 - 0
apps/search/src/search/urls.py

@@ -19,6 +19,8 @@ from django.conf.urls.defaults import patterns, url
 
 urlpatterns = patterns('search.views',
   url(r'^$', 'index', name='index'),
+  url(r'^index2$', 'index2', name='index2'),
+  url(r'^query2$', 'index2', name='query2'),
   url(r'^query$', 'index', name='query'),
   url(r'^download/(?P<format>(csv|xls))$', 'download', name='download'),
 

+ 1241 - 2
apps/search/src/search/views.py

@@ -34,10 +34,11 @@ from search.data_export import download as export_download
 from search.decorators import allow_admin_only
 from search.forms import QueryForm, CollectionForm
 from search.management.commands import search_setup
-from search.models import Collection, augment_solr_response
+from search.models import Collection, augment_solr_response, augment_solr_response2
 from search.search_controller import SearchController
 
 from django.utils.encoding import force_unicode
+from desktop.lib.rest.http_client import RestException
 
 
 LOG = logging.getLogger(__name__)
@@ -80,7 +81,7 @@ def index(request):
     error['message'] = _('There is no collection to search.')
 
   if hue_collection is not None:
-    response = augment_solr_response(response, hue_collection.facets.get_data())
+    response = augment_solr_response(response, hue_collection.facets.get_data(), solr_query)
 
   if request.GET.get('format') == 'json':
     return HttpResponse(json.dumps(response), mimetype="application/json")
@@ -99,6 +100,100 @@ def dashboard(request):
   return render('dashboard.mako', request, {})
 
 
+def index2(request):
+  hue_collections = SearchController(request.user).get_search_collections()
+
+  if not hue_collections:
+    if request.user.is_superuser:
+      return admin_collections(request, True)
+    else:
+      return no_collections(request)
+
+  init_collection = initial_collection(request, hue_collections)
+
+  search_form = QueryForm(request.POST, initial_collection=init_collection)
+  response = {}
+  solr_query = {}
+
+  if search_form.is_valid():    
+    try:
+      collection_id = search_form.cleaned_data['collection']
+      hue_collection = Collection.objects.get(id=collection_id)
+
+      solr_query = search_form.solr_query_dict
+      print request.POST
+      # if selected facets --> add fq fields + exlcude tag y/n
+      fcets = json.loads(request.POST.get('facets', '[]'))
+      solr_query['fq'] = json.loads(request.POST.get('fq', '{}'))
+      solr_query['q'] = json.loads(request.POST.get('q', '""'))
+      template = json.loads(request.POST.get('template', '{}'))
+      #solr_query['fl'] = template.get('fields', []) # if we do this, need to parse the template and fill up the fields list
+      print solr_query
+      print fcets, '===='
+      response = SolrApi(SOLR_URL.get(), request.user).query2(solr_query, fcets)
+
+      if hue_collection is not None:
+        response = augment_solr_response2(response, fcets, solr_query)
+      solr_query['total_pages'] = int(math.ceil((float(response['response']['numFound']) / float(solr_query['rows']))))
+      solr_query['search_time'] = response['responseHeader']['QTime']
+    except RestException, e:
+      try:
+        response['error'] = json.loads(e.message)['error']['msg']
+      except:
+        response['error'] = force_unicode(str(e))
+    except Exception, e:
+      raise PopupException(e, title=_('Error while accessing Solr'))
+      
+      response['error'] = force_unicode(str(e))
+  else:
+    response['error'] = _('There is no collection to search.')
+
+  if 'error' in response:
+    response.update(
+    {
+      "facet_counts": {   
+      },
+      "highlighting": {
+      },
+      "normalized_facets": [
+        {
+          "field": "user_location",
+          "counts": [],
+          "type": "field",
+          "label": "Location"
+        },
+        {
+          "field": "not_there",
+          "counts": [],
+          "type": "field",
+          "label": "Bad facet"
+        },
+      ],
+      "responseHeader": {
+        "status": -1,
+        "QTime": 0,
+        "params": {
+        }
+      },
+      "response": {
+        "start": 0,
+        "numFound": 0,
+        "docs": [
+        ]
+      }
+    }) 
+
+  if request.GET.get('format') == 'json':
+    return HttpResponse(json.dumps(response), mimetype="application/json")
+
+  return render('search2.mako', request, {
+    'search_form': search_form,
+    'response': json.dumps(response),
+    'solr_query': solr_query,
+    'hue_collection': hue_collection,
+    'current_collection': collection_id,
+  })
+
 def download(request, format):
   hue_collections = SearchController(request.user).get_search_collections()
 
@@ -412,3 +507,1147 @@ def install_examples(request):
       result['message'] = str(e)
 
   return HttpResponse(json.dumps(result), mimetype="application/json")
+
+MOCK = """{
+  "facet_counts": {
+    "facet_ranges": {
+      "created_at": {
+        "start": "2014-02-18T12:00:00Z",
+        "counts": [
+          "2014-02-25T16:05:00Z",
+          97,
+          "2014-02-25T16:10:00Z",
+          92,
+          "2014-02-25T16:15:00Z",
+          115,
+          "2014-02-25T16:20:00Z",
+          83,
+          "2014-02-25T16:25:00Z",
+          108,
+          "2014-02-25T16:30:00Z",
+          120,
+          "2014-02-25T16:35:00Z",
+          98,
+          "2014-02-25T16:40:00Z",
+          101,
+          "2014-02-25T16:45:00Z",
+          110,
+          "2014-02-25T16:50:00Z",
+          100,
+          "2014-02-25T16:55:00Z",
+          96,
+          "2014-02-25T17:00:00Z",
+          119,
+          "2014-02-25T17:05:00Z",
+          125,
+          "2014-02-25T17:10:00Z",
+          115,
+          "2014-02-25T17:15:00Z",
+          116,
+          "2014-02-25T17:20:00Z",
+          107,
+          "2014-02-25T17:25:00Z",
+          139,
+          "2014-02-25T17:30:00Z",
+          131,
+          "2014-02-25T17:35:00Z",
+          123,
+          "2014-02-25T17:40:00Z",
+          125,
+          "2014-02-25T17:45:00Z",
+          125,
+          "2014-02-25T17:50:00Z",
+          102,
+          "2014-02-25T17:55:00Z",
+          149,
+          "2014-02-25T18:00:00Z",
+          130,
+          "2014-02-25T18:05:00Z",
+          123,
+          "2014-02-25T18:10:00Z",
+          145,
+          "2014-02-25T18:15:00Z",
+          108,
+          "2014-02-25T18:20:00Z",
+          133,
+          "2014-02-25T18:25:00Z",
+          132,
+          "2014-02-25T18:30:00Z",
+          162,
+          "2014-02-25T18:35:00Z",
+          142,
+          "2014-02-25T18:40:00Z",
+          139,
+          "2014-02-25T18:45:00Z",
+          156,
+          "2014-02-25T18:50:00Z",
+          132,
+          "2014-02-25T18:55:00Z",
+          159,
+          "2014-02-25T19:00:00Z",
+          157,
+          "2014-02-25T19:05:00Z",
+          136,
+          "2014-02-25T19:10:00Z",
+          137,
+          "2014-02-25T19:15:00Z",
+          164,
+          "2014-02-25T19:20:00Z",
+          132,
+          "2014-02-25T19:25:00Z",
+          154,
+          "2014-02-25T19:30:00Z",
+          187,
+          "2014-02-25T19:35:00Z",
+          161,
+          "2014-02-25T19:40:00Z",
+          159,
+          "2014-02-25T19:45:00Z",
+          144,
+          "2014-02-25T19:50:00Z",
+          157,
+          "2014-02-25T19:55:00Z",
+          146,
+          "2014-02-25T20:00:00Z",
+          193,
+          "2014-02-25T20:05:00Z",
+          175,
+          "2014-02-25T20:10:00Z",
+          189,
+          "2014-02-25T20:15:00Z",
+          182,
+          "2014-02-25T20:20:00Z",
+          168,
+          "2014-02-25T20:25:00Z",
+          171,
+          "2014-02-25T20:30:00Z",
+          175,
+          "2014-02-25T20:35:00Z",
+          169,
+          "2014-02-25T20:40:00Z",
+          182,
+          "2014-02-25T20:45:00Z",
+          163,
+          "2014-02-25T20:50:00Z",
+          186,
+          "2014-02-25T20:55:00Z",
+          184,
+          "2014-02-25T21:00:00Z",
+          216,
+          "2014-02-25T21:05:00Z",
+          202,
+          "2014-02-25T21:10:00Z",
+          176,
+          "2014-02-25T21:15:00Z",
+          196,
+          "2014-02-25T21:20:00Z",
+          192,
+          "2014-02-25T21:25:00Z",
+          189,
+          "2014-02-25T21:30:00Z",
+          190,
+          "2014-02-25T21:35:00Z",
+          187,
+          "2014-02-25T21:40:00Z",
+          215,
+          "2014-02-25T21:45:00Z",
+          197,
+          "2014-02-25T21:50:00Z",
+          174,
+          "2014-02-25T21:55:00Z",
+          179,
+          "2014-02-25T22:00:00Z",
+          216,
+          "2014-02-25T22:05:00Z",
+          185,
+          "2014-02-25T22:10:00Z",
+          178,
+          "2014-02-25T22:15:00Z",
+          219,
+          "2014-02-25T22:20:00Z",
+          190,
+          "2014-02-25T22:25:00Z",
+          190,
+          "2014-02-25T22:30:00Z",
+          196,
+          "2014-02-25T22:35:00Z",
+          176,
+          "2014-02-25T22:40:00Z",
+          215,
+          "2014-02-25T22:45:00Z",
+          212,
+          "2014-02-25T22:50:00Z",
+          200,
+          "2014-02-25T22:55:00Z",
+          216,
+          "2014-02-25T23:00:00Z",
+          225,
+          "2014-02-25T23:05:00Z",
+          186,
+          "2014-02-25T23:10:00Z",
+          181,
+          "2014-02-25T23:15:00Z",
+          213,
+          "2014-02-25T23:20:00Z",
+          214,
+          "2014-02-25T23:25:00Z",
+          189,
+          "2014-02-25T23:30:00Z",
+          188,
+          "2014-02-25T23:35:00Z",
+          174,
+          "2014-02-25T23:40:00Z",
+          199,
+          "2014-02-25T23:45:00Z",
+          165,
+          "2014-02-25T23:50:00Z",
+          198,
+          "2014-02-25T23:55:00Z",
+          158,
+          "2014-02-26T00:00:00Z",
+          193,
+          "2014-02-26T00:05:00Z",
+          168,
+          "2014-02-26T00:10:00Z",
+          186,
+          "2014-02-26T00:15:00Z",
+          182,
+          "2014-02-26T00:20:00Z",
+          174,
+          "2014-02-26T00:25:00Z",
+          185,
+          "2014-02-26T00:30:00Z",
+          184,
+          "2014-02-26T00:35:00Z",
+          157,
+          "2014-02-26T00:40:00Z",
+          161,
+          "2014-02-26T00:45:00Z",
+          158,
+          "2014-02-26T00:50:00Z",
+          175,
+          "2014-02-26T00:55:00Z",
+          151,
+          "2014-02-26T01:00:00Z",
+          203,
+          "2014-02-26T01:05:00Z",
+          154,
+          "2014-02-26T01:10:00Z",
+          158,
+          "2014-02-26T01:15:00Z",
+          153,
+          "2014-02-26T01:20:00Z",
+          141,
+          "2014-02-26T01:25:00Z",
+          150,
+          "2014-02-26T01:30:00Z",
+          165,
+          "2014-02-26T01:35:00Z",
+          152,
+          "2014-02-26T01:40:00Z",
+          161,
+          "2014-02-26T01:45:00Z",
+          178,
+          "2014-02-26T01:50:00Z",
+          145,
+          "2014-02-26T01:55:00Z",
+          161,
+          "2014-02-26T02:00:00Z",
+          171,
+          "2014-02-26T02:05:00Z",
+          151,
+          "2014-02-26T02:10:00Z",
+          141,
+          "2014-02-26T02:15:00Z",
+          145,
+          "2014-02-26T02:20:00Z",
+          149,
+          "2014-02-26T02:25:00Z",
+          131,
+          "2014-02-26T02:30:00Z",
+          134,
+          "2014-02-26T02:35:00Z",
+          142,
+          "2014-02-26T02:40:00Z",
+          133,
+          "2014-02-26T02:45:00Z",
+          157,
+          "2014-02-26T02:50:00Z",
+          154,
+          "2014-02-26T02:55:00Z",
+          146,
+          "2014-02-26T03:00:00Z",
+          124,
+          "2014-02-26T03:05:00Z",
+          147,
+          "2014-02-26T03:10:00Z",
+          142,
+          "2014-02-26T03:15:00Z",
+          137,
+          "2014-02-26T03:20:00Z",
+          139,
+          "2014-02-26T03:25:00Z",
+          156,
+          "2014-02-26T03:30:00Z",
+          18,
+          "2014-02-26T04:10:00Z",
+          15,
+          "2014-02-26T04:15:00Z",
+          65,
+          "2014-02-26T04:20:00Z",
+          53,
+          "2014-02-26T04:25:00Z",
+          66,
+          "2014-02-26T04:30:00Z",
+          65,
+          "2014-02-26T04:35:00Z",
+          57,
+          "2014-02-26T04:40:00Z",
+          61
+        ],
+        "end": "2014-02-28T12:00:00Z",
+        "gap": "+5MINUTES"
+      },
+      "user_followers_count": {
+        "start": 0,
+        "counts": [
+          "0",
+          4585,
+          "100",
+          3725,
+          "200",
+          2719,
+          "300",
+          1881,
+          "400",
+          1346,
+          "500",
+          966,
+          "600",
+          709,
+          "700",
+          668,
+          "800",
+          448,
+          "900",
+          383
+        ],
+        "end": 1000,
+        "gap": 100
+      },
+      "user_statuses_count": {
+        "start": 0,
+        "counts": [
+          "0",
+          3981,
+          "1000",
+          2223,
+          "2000",
+          1701,
+          "3000",
+          1270,
+          "4000",
+          1051,
+          "5000",
+          922,
+          "6000",
+          784,
+          "7000",
+          770,
+          "8000",
+          603,
+          "9000",
+          587
+        ],
+        "end": 10000,
+        "gap": 1000
+      }
+    },
+    "facet_fields": {
+      "user_location": [
+        "indonesia",
+        2897,
+        "venezuela",
+        1798,
+        "london",
+        1783,
+        "istanbul",
+        1674,
+        "philippines",
+        1284,
+        "argentina",
+        1199,
+        "brasil",
+        1017,
+        "thailand",
+        1009,
+        "jakarta",
+        912,
+        "paris",
+        911,
+        "uk",
+        902,
+        "france",
+        862,
+        "\u6771\u4eac",
+        809,
+        "malaysia",
+        758,
+        "japan",
+        719,
+        "usa",
+        696,
+        "madrid",
+        680,
+        "espa\u00f1a",
+        659,
+        "\u5927\u962a",
+        591,
+        "new york",
+        581,
+        "t\u00fcrkiye",
+        477
+      ]
+    },
+    "facet_dates": {},
+    "facet_queries": {}
+  },
+  "highlighting": {
+    "438585496994725888": {},
+    "438585509556658176": {},
+    "438585555710791680": {},
+    "438585614410063872": {},
+    "438585664741703681": {},
+    "438585618617352192": {},
+    "438585568302489600": {},
+    "438585593438560256": {},
+    "438585606034046976": {},
+    "438585664745906176": {},
+    "438585526355238912": {},
+    "438585601831763968": {},
+    "438585618612756480": {},
+    "438585501180653568": {},
+    "438585580856045568": {}
+  },
+  "normalized_facets": [
+    {
+      "field": "user_location",
+      "counts": [
+        "indonesia",
+        2897,
+        "venezuela",
+        1798,
+        "london",
+        1783,
+        "istanbul",
+        1674,
+        "philippines",
+        1284,
+        "argentina",
+        1199,
+        "brasil",
+        1017,
+        "thailand",
+        1009,
+        "jakarta",
+        912,
+        "paris",
+        911,
+        "uk",
+        902,
+        "france",
+        862,
+        "\u6771\u4eac",
+        809,
+        "malaysia",
+        758,
+        "japan",
+        719,
+        "usa",
+        696,
+        "madrid",
+        680,
+        "espa\u00f1a",
+        659,
+        "\u5927\u962a",
+        591,
+        "new york",
+        581,
+        "t\u00fcrkiye",
+        477
+      ],
+      "type": "field",
+      "label": "Location"
+    },
+    {
+      "end": 1000,
+      "start": 0,
+      "label": "Followers count",
+      "field": "user_followers_count",
+      "counts": [
+        "0",
+        4585,
+        "100",
+        3725,
+        "200",
+        2719,
+        "300",
+        1881,
+        "400",
+        1346,
+        "500",
+        966,
+        "600",
+        709,
+        "700",
+        668,
+        "800",
+        448,
+        "900",
+        383
+      ],
+      "gap": 100,
+      "type": "range"
+    },
+    {
+      "end": 10000,
+      "start": 0,
+      "label": "Tweet count",
+      "field": "user_statuses_count",
+      "counts": [
+        "0",
+        3981,
+        "1000",
+        2223,
+        "2000",
+        1701,
+        "3000",
+        1270,
+        "4000",
+        1051,
+        "5000",
+        922,
+        "6000",
+        784,
+        "7000",
+        770,
+        "8000",
+        603,
+        "9000",
+        587
+      ],
+      "gap": 1000,
+      "type": "range"
+    },
+    {
+      "end": "2014-02-28T12:00:00Z",
+      "start": "2014-02-18T12:00:00Z",
+      "label": "created_at",
+      "field": "created_at",
+      "counts": [
+        "2014-02-25T16:05:00Z",
+        97,
+        "2014-02-25T16:10:00Z",
+        92,
+        "2014-02-25T16:15:00Z",
+        115,
+        "2014-02-25T16:20:00Z",
+        83,
+        "2014-02-25T16:25:00Z",
+        108,
+        "2014-02-25T16:30:00Z",
+        120,
+        "2014-02-25T16:35:00Z",
+        98,
+        "2014-02-25T16:40:00Z",
+        101,
+        "2014-02-25T16:45:00Z",
+        110,
+        "2014-02-25T16:50:00Z",
+        100,
+        "2014-02-25T16:55:00Z",
+        96,
+        "2014-02-25T17:00:00Z",
+        119,
+        "2014-02-25T17:05:00Z",
+        125,
+        "2014-02-25T17:10:00Z",
+        115,
+        "2014-02-25T17:15:00Z",
+        116,
+        "2014-02-25T17:20:00Z",
+        107,
+        "2014-02-25T17:25:00Z",
+        139,
+        "2014-02-25T17:30:00Z",
+        131,
+        "2014-02-25T17:35:00Z",
+        123,
+        "2014-02-25T17:40:00Z",
+        125,
+        "2014-02-25T17:45:00Z",
+        125,
+        "2014-02-25T17:50:00Z",
+        102,
+        "2014-02-25T17:55:00Z",
+        149,
+        "2014-02-25T18:00:00Z",
+        130,
+        "2014-02-25T18:05:00Z",
+        123,
+        "2014-02-25T18:10:00Z",
+        145,
+        "2014-02-25T18:15:00Z",
+        108,
+        "2014-02-25T18:20:00Z",
+        133,
+        "2014-02-25T18:25:00Z",
+        132,
+        "2014-02-25T18:30:00Z",
+        162,
+        "2014-02-25T18:35:00Z",
+        142,
+        "2014-02-25T18:40:00Z",
+        139,
+        "2014-02-25T18:45:00Z",
+        156,
+        "2014-02-25T18:50:00Z",
+        132,
+        "2014-02-25T18:55:00Z",
+        159,
+        "2014-02-25T19:00:00Z",
+        157,
+        "2014-02-25T19:05:00Z",
+        136,
+        "2014-02-25T19:10:00Z",
+        137,
+        "2014-02-25T19:15:00Z",
+        164,
+        "2014-02-25T19:20:00Z",
+        132,
+        "2014-02-25T19:25:00Z",
+        154,
+        "2014-02-25T19:30:00Z",
+        187,
+        "2014-02-25T19:35:00Z",
+        161,
+        "2014-02-25T19:40:00Z",
+        159,
+        "2014-02-25T19:45:00Z",
+        144,
+        "2014-02-25T19:50:00Z",
+        157,
+        "2014-02-25T19:55:00Z",
+        146,
+        "2014-02-25T20:00:00Z",
+        193,
+        "2014-02-25T20:05:00Z",
+        175,
+        "2014-02-25T20:10:00Z",
+        189,
+        "2014-02-25T20:15:00Z",
+        182,
+        "2014-02-25T20:20:00Z",
+        168,
+        "2014-02-25T20:25:00Z",
+        171,
+        "2014-02-25T20:30:00Z",
+        175,
+        "2014-02-25T20:35:00Z",
+        169,
+        "2014-02-25T20:40:00Z",
+        182,
+        "2014-02-25T20:45:00Z",
+        163,
+        "2014-02-25T20:50:00Z",
+        186,
+        "2014-02-25T20:55:00Z",
+        184,
+        "2014-02-25T21:00:00Z",
+        216,
+        "2014-02-25T21:05:00Z",
+        202,
+        "2014-02-25T21:10:00Z",
+        176,
+        "2014-02-25T21:15:00Z",
+        196,
+        "2014-02-25T21:20:00Z",
+        192,
+        "2014-02-25T21:25:00Z",
+        189,
+        "2014-02-25T21:30:00Z",
+        190,
+        "2014-02-25T21:35:00Z",
+        187,
+        "2014-02-25T21:40:00Z",
+        215,
+        "2014-02-25T21:45:00Z",
+        197,
+        "2014-02-25T21:50:00Z",
+        174,
+        "2014-02-25T21:55:00Z",
+        179,
+        "2014-02-25T22:00:00Z",
+        216,
+        "2014-02-25T22:05:00Z",
+        185,
+        "2014-02-25T22:10:00Z",
+        178,
+        "2014-02-25T22:15:00Z",
+        219,
+        "2014-02-25T22:20:00Z",
+        190,
+        "2014-02-25T22:25:00Z",
+        190,
+        "2014-02-25T22:30:00Z",
+        196,
+        "2014-02-25T22:35:00Z",
+        176,
+        "2014-02-25T22:40:00Z",
+        215,
+        "2014-02-25T22:45:00Z",
+        212,
+        "2014-02-25T22:50:00Z",
+        200,
+        "2014-02-25T22:55:00Z",
+        216,
+        "2014-02-25T23:00:00Z",
+        225,
+        "2014-02-25T23:05:00Z",
+        186,
+        "2014-02-25T23:10:00Z",
+        181,
+        "2014-02-25T23:15:00Z",
+        213,
+        "2014-02-25T23:20:00Z",
+        214,
+        "2014-02-25T23:25:00Z",
+        189,
+        "2014-02-25T23:30:00Z",
+        188,
+        "2014-02-25T23:35:00Z",
+        174,
+        "2014-02-25T23:40:00Z",
+        199,
+        "2014-02-25T23:45:00Z",
+        165,
+        "2014-02-25T23:50:00Z",
+        198,
+        "2014-02-25T23:55:00Z",
+        158,
+        "2014-02-26T00:00:00Z",
+        193,
+        "2014-02-26T00:05:00Z",
+        168,
+        "2014-02-26T00:10:00Z",
+        186,
+        "2014-02-26T00:15:00Z",
+        182,
+        "2014-02-26T00:20:00Z",
+        174,
+        "2014-02-26T00:25:00Z",
+        185,
+        "2014-02-26T00:30:00Z",
+        184,
+        "2014-02-26T00:35:00Z",
+        157,
+        "2014-02-26T00:40:00Z",
+        161,
+        "2014-02-26T00:45:00Z",
+        158,
+        "2014-02-26T00:50:00Z",
+        175,
+        "2014-02-26T00:55:00Z",
+        151,
+        "2014-02-26T01:00:00Z",
+        203,
+        "2014-02-26T01:05:00Z",
+        154,
+        "2014-02-26T01:10:00Z",
+        158,
+        "2014-02-26T01:15:00Z",
+        153,
+        "2014-02-26T01:20:00Z",
+        141,
+        "2014-02-26T01:25:00Z",
+        150,
+        "2014-02-26T01:30:00Z",
+        165,
+        "2014-02-26T01:35:00Z",
+        152,
+        "2014-02-26T01:40:00Z",
+        161,
+        "2014-02-26T01:45:00Z",
+        178,
+        "2014-02-26T01:50:00Z",
+        145,
+        "2014-02-26T01:55:00Z",
+        161,
+        "2014-02-26T02:00:00Z",
+        171,
+        "2014-02-26T02:05:00Z",
+        151,
+        "2014-02-26T02:10:00Z",
+        141,
+        "2014-02-26T02:15:00Z",
+        145,
+        "2014-02-26T02:20:00Z",
+        149,
+        "2014-02-26T02:25:00Z",
+        131,
+        "2014-02-26T02:30:00Z",
+        134,
+        "2014-02-26T02:35:00Z",
+        142,
+        "2014-02-26T02:40:00Z",
+        133,
+        "2014-02-26T02:45:00Z",
+        157,
+        "2014-02-26T02:50:00Z",
+        154,
+        "2014-02-26T02:55:00Z",
+        146,
+        "2014-02-26T03:00:00Z",
+        124,
+        "2014-02-26T03:05:00Z",
+        147,
+        "2014-02-26T03:10:00Z",
+        142,
+        "2014-02-26T03:15:00Z",
+        137,
+        "2014-02-26T03:20:00Z",
+        139,
+        "2014-02-26T03:25:00Z",
+        156,
+        "2014-02-26T03:30:00Z",
+        18,
+        "2014-02-26T04:10:00Z",
+        15,
+        "2014-02-26T04:15:00Z",
+        65,
+        "2014-02-26T04:20:00Z",
+        53,
+        "2014-02-26T04:25:00Z",
+        66,
+        "2014-02-26T04:30:00Z",
+        65,
+        "2014-02-26T04:35:00Z",
+        57,
+        "2014-02-26T04:40:00Z",
+        61
+      ],
+      "gap": "+5MINUTES",
+      "type": "chart"
+    }
+  ],
+  "responseHeader": {
+    "status": 0,
+    "QTime": 248,
+    "params": {
+      "f.created_at.facet.range.start": "2014-02-28T12:00:00Z-10DAYS",
+      "f.created_at.facet.range.gap": "+5MINUTES",
+      "f.user_followers_count.facet.range.start": "0",
+      "facet": "true",
+      "facet.mincount": "1",
+      "rows": "15",
+      "f.user_statuses_count.facet.range.gap": "1000",
+      "doAs": "romain",
+      "start": "0",
+      "user.name": "hue",
+      "f.created_at.facet.range.end": "2014-02-28T12:00:00Z",
+      "f.user_statuses_count.facet.range.start": "0",
+      "facet.field": "user_location",
+      "wt": "json",
+      "hl": "true",
+      "hl.fl": "text",
+      "f.user_followers_count.facet.range.gap": "100",
+      "facet.sort": "count",
+      "f.user_statuses_count.facet.range.end": "10000",
+      "f.user_followers_count.facet.range.end": "1000",
+      "facet.limit": "100",
+      "facet.range": [
+        "created_at",
+        "user_followers_count",
+        "user_statuses_count"
+      ],
+      "f.created_at.facet.limit": "-1",
+      "q": "*:*"
+    }
+  },
+  "response": {
+    "start": 0,
+    "numFound": 22218,
+    "docs": [
+      {
+        "created_at": "2014-02-25T16:05:05Z",
+        "user_followers_count": 1897,
+        "text": "RT @Fact: People who are strongly connected with their friends have stronger immune system than those who keep themselves isolated.",
+        "user_screen_name": "Jade___Richards",
+        "user_location": "UK",
+        "user_statuses_count": 75045,
+        "source": "<a href=\"http://dlvr.it\" rel=\"nofollow\">dlvr.it</a>",
+        "in_reply_to_status_id": [
+          -1
+        ],
+        "in_reply_to_user_id": -1,
+        "_version_": 1461767321259343872,
+        "retweet_count": 0,
+        "user_name": "Jade Richards News",
+        "id": "438585496994725888",
+        "user_friends_count": 2617
+      },
+      {
+        "created_at": "2014-02-25T16:05:08Z",
+        "user_followers_count": 39,
+        "text": "Udh dimanee@nimnimc",
+        "user_screen_name": "vergiawanlista2",
+        "user_location": "JAKARTA",
+        "user_statuses_count": 136,
+        "source": "<a href=\"http://blackberry.com/twitter\" rel=\"nofollow\">Twitter for BlackBerry\u00ae</a>",
+        "in_reply_to_status_id": [
+          -1
+        ],
+        "in_reply_to_user_id": -1,
+        "_version_": 1461767321313869824,
+        "retweet_count": 0,
+        "user_name": "VergiawanLisTanto",
+        "id": "438585509556658176",
+        "user_friends_count": 121
+      },
+      {
+        "created_at": "2014-02-25T16:05:06Z",
+        "user_followers_count": 140,
+        "text": "Emo. huwaaaaa... T.T",
+        "user_screen_name": "nickdayah",
+        "user_location": "Malaysia",
+        "user_statuses_count": 8777,
+        "source": "<a href=\"https://twitter.com/download/android\" rel=\"nofollow\">Twitter for  Android</a>",
+        "in_reply_to_status_id": [
+          -1
+        ],
+        "in_reply_to_user_id": -1,
+        "_version_": 1461767321315966976,
+        "retweet_count": 0,
+        "user_name": "Dayah Badri",
+        "id": "438585501180653568",
+        "user_friends_count": 144
+      },
+      {
+        "created_at": "2014-02-25T16:05:19Z",
+        "user_followers_count": 124,
+        "text": "RT @2TheHacker_: hati hati dengan saya saya memauntau anda",
+        "user_screen_name": "dayantiday",
+        "user_location": "indonesia",
+        "user_statuses_count": 7355,
+        "source": "<a href=\"http://www.twitter.com\" rel=\"nofollow\">Sistem Autentikasi</a>",
+        "in_reply_to_status_id": [
+          -1
+        ],
+        "in_reply_to_user_id": -1,
+        "_version_": 1461767321318064128,
+        "retweet_count": 0,
+        "user_name": "dayanti sukmawati",
+        "id": "438585555710791680",
+        "user_friends_count": 2002
+      },
+      {
+        "created_at": "2014-02-25T16:05:12Z",
+        "user_followers_count": 14,
+        "text": "RT @cosythirlwall: The girls have to win this, rt rt rt #VoteLittleMixUK #KCA",
+        "user_screen_name": "glitterstars98",
+        "user_location": "London",
+        "user_statuses_count": 468,
+        "source": "<a href=\"http://twitter.com/download/android\" rel=\"nofollow\">Twitter for Android</a>",
+        "in_reply_to_status_id": [
+          -1
+        ],
+        "in_reply_to_user_id": -1,
+        "_version_": 1461767321320161280,
+        "retweet_count": 0,
+        "user_name": "LM are my idols",
+        "id": "438585526355238912",
+        "user_friends_count": 28
+      },
+      {
+        "created_at": "2014-02-25T16:05:30Z",
+        "user_followers_count": 2666,
+        "text": "Toriii (Red) Gates number 995 - 1000 @reginachristian #prettygirl #livebold #liveglorious\u2026 http://t.co/1LXuzzvmmk",
+        "user_screen_name": "evantjandra",
+        "user_location": "Jakarta",
+        "user_statuses_count": 4439,
+        "source": "<a href=\"http://instagram.com\" rel=\"nofollow\">Instagram</a>",
+        "in_reply_to_status_id": [
+          -1
+        ],
+        "in_reply_to_user_id": -1,
+        "_version_": 1461767321322258432,
+        "retweet_count": 0,
+        "user_name": "Evan Tjandra",
+        "id": "438585601831763968",
+        "user_friends_count": 587
+      },
+      {
+        "created_at": "2014-02-25T16:05:31Z",
+        "user_followers_count": 4258,
+        "text": "\u5f15\u3063\u8d8a\u3057\u306e\u4e00\u62ec\u898b\u7a4d\u3082\u308a\u3057\u3066\u2026\u55b6\u696d\u306e\u96fb\u8a71\u304c\u5acc\u3067\u3059\u3088\u306d\uff61\u696d\u754c\u521d\uff01\u500b\u4eba\u60c5\u5831\u3092\u4f0f\u305b\u305f\u4e0a\u3067\u3001\u5f15\u8d8a\u696d\u8005\u62c5\u5f53\u8005\u3068\u30c1\u30e3\u30c3\u30c8\u3067\u3084\u308a\u53d6\u308a\u304c\u51fa\u6765\u308b\u30b5\u30a4\u30c8\u306f\u3053\u3053\u3060\u3051\uff01 http://t.co/OsRUDcfQTM",
+        "user_screen_name": "otoku_matome",
+        "user_location": "japan",
+        "user_statuses_count": 15614,
+        "source": "<a href=\"http://twittbot.net/\" rel=\"nofollow\">twittbot.net</a>",
+        "in_reply_to_status_id": [
+          -1
+        ],
+        "in_reply_to_user_id": -1,
+        "_version_": 1461767321324355584,
+        "retweet_count": 0,
+        "user_name": "\u304a\u5f97\u60c5\u5831\u307e\u3068\u3081",
+        "id": "438585606034046976",
+        "user_friends_count": 4016
+      },
+      {
+        "created_at": "2014-02-25T16:05:28Z",
+        "user_followers_count": 248,
+        "text": "@TeladanRasul: Jgn kalian saling membenci,jangan saling hasad,jangan saling membelakangi,jangan saling memutuskan silaturrahim (HR Muslim)",
+        "user_screen_name": "SATGASiti",
+        "user_location": "Indonesia",
+        "user_statuses_count": 1776,
+        "source": "<a href=\"https://twitter.com/download/android\" rel=\"nofollow\">Twitter for  Android</a>",
+        "in_reply_to_status_id": [
+          -1
+        ],
+        "in_reply_to_user_id": 213140358,
+        "_version_": 1461767321324355585,
+        "retweet_count": 0,
+        "user_name": "Nana\u2665",
+        "id": "438585593438560256",
+        "user_friends_count": 299
+      },
+      {
+        "created_at": "2014-02-25T16:05:33Z",
+        "user_followers_count": 103,
+        "text": "It hurts, because I'm so lonely so I say I'm missing you. 2NE1's slow songs.. *cry*",
+        "user_screen_name": "ahyu_savitri",
+        "user_location": "Indonesia",
+        "user_statuses_count": 268,
+        "source": "<a href=\"https://twitter.com/download/android\" rel=\"nofollow\">Twitter for  Android</a>",
+        "in_reply_to_status_id": [
+          -1
+        ],
+        "in_reply_to_user_id": -1,
+        "_version_": 1461767321325404160,
+        "retweet_count": 0,
+        "user_name": "Savitri",
+        "id": "438585614410063872",
+        "user_friends_count": 347
+      },
+      {
+        "created_at": "2014-02-25T16:05:25Z",
+        "user_followers_count": 140,
+        "text": "Dustin O'Halloran - An Ending, A Beginning http://t.co/jafiD40DFr",
+        "user_screen_name": "adadidem",
+        "user_location": "\u0130stanbul",
+        "user_statuses_count": 11670,
+        "source": "<a href=\"http://www.apple.com\" rel=\"nofollow\">iOS</a>",
+        "in_reply_to_status_id": [
+          -1
+        ],
+        "in_reply_to_user_id": -1,
+        "_version_": 1461767321325404161,
+        "retweet_count": 0,
+        "user_name": "Didem Saritas",
+        "id": "438585580856045568",
+        "user_friends_count": 287
+      },
+      {
+        "created_at": "2014-02-25T16:05:22Z",
+        "user_followers_count": 554,
+        "text": "Me tiene que sonar la alarma a las 11:30 para tomar la pasti, espero despertarme U.U",
+        "user_screen_name": "Badgalcamm",
+        "user_location": "Argentina",
+        "user_statuses_count": 19242,
+        "source": "<a href=\"http://twitter.com/download/android\" rel=\"nofollow\">Twitter for Android</a>",
+        "in_reply_to_status_id": [
+          -1
+        ],
+        "in_reply_to_user_id": -1,
+        "_version_": 1461767321326452736,
+        "retweet_count": 0,
+        "user_name": "Camila\u2661",
+        "id": "438585568302489600",
+        "user_friends_count": 667
+      },
+      {
+        "created_at": "2014-02-25T16:05:34Z",
+        "user_followers_count": 33,
+        "text": "RT @2TheHacker_: hati hati dengan saya saya memauntau anda",
+        "user_screen_name": "uzie_oz",
+        "user_location": "indonesia",
+        "user_statuses_count": 5419,
+        "source": "<a href=\"http://www.twitter.com\" rel=\"nofollow\">Sistem Autentikasi</a>",
+        "in_reply_to_status_id": [
+          -1
+        ],
+        "in_reply_to_user_id": -1,
+        "_version_": 1461767321326452737,
+        "retweet_count": 0,
+        "user_name": "ahmad fauji",
+        "id": "438585618612756480",
+        "user_friends_count": 2002
+      },
+      {
+        "created_at": "2014-02-25T16:05:34Z",
+        "user_followers_count": 86,
+        "text": "En Espa\u00f1a existen tres tipos diferentes de legislaci\u00f3n sobre el r\u00e9gimen econ\u00f3mico matrimonial http://t.co/SVxwfCaYDS",
+        "user_screen_name": "DGAabogados",
+        "user_location": "Madrid",
+        "user_statuses_count": 535,
+        "source": "<a href=\"http://twitter.com/download/android\" rel=\"nofollow\">Twitter for Android</a>",
+        "in_reply_to_status_id": [
+          -1
+        ],
+        "in_reply_to_user_id": -1,
+        "_version_": 1461767321327501312,
+        "retweet_count": 0,
+        "user_name": "DGA Abogados",
+        "id": "438585618617352192",
+        "user_friends_count": 201
+      },
+      {
+        "created_at": "2014-02-25T16:05:45Z",
+        "user_followers_count": 547,
+        "text": "Dan ga seharusnya mslh beginian gw yg ngadepin!!! Yg gw tau,gw cm ngurusin mslh DUIT dan kelancaran operasional divisi kitchen !!!!",
+        "user_screen_name": "nona_etty",
+        "user_location": "jakarta",
+        "user_statuses_count": 1018,
+        "source": "<a href=\"http://ubersocial.com\" rel=\"nofollow\">UberSocial for Android</a>",
+        "in_reply_to_status_id": [
+          -1
+        ],
+        "in_reply_to_user_id": -1,
+        "_version_": 1461767321329598464,
+        "retweet_count": 0,
+        "user_name": "Etty_Sundari",
+        "id": "438585664741703681",
+        "user_friends_count": 95
+      },
+      {
+        "created_at": "2014-02-25T16:05:45Z",
+        "user_followers_count": 267,
+        "text": "This is real",
+        "user_screen_name": "dinimslm",
+        "user_location": "Malaysia",
+        "user_statuses_count": 14817,
+        "source": "<a href=\"http://twitter.com/download/android\" rel=\"nofollow\">Twitter for Android</a>",
+        "in_reply_to_status_id": [
+          -1
+        ],
+        "in_reply_to_user_id": -1,
+        "_version_": 1461767321330647040,
+        "retweet_count": 0,
+        "user_name": "aurora ",
+        "id": "438585664745906176",
+        "user_friends_count": 649
+      }
+    ]
+  }
+}"""

+ 55 - 0
apps/search/static/js/query.ko.js

@@ -0,0 +1,55 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+function QueryViewModel(json_tags, json_docs) {
+  var self = this;
+
+  var MOCK_TAGS = {
+    'history': {'name': 'History', 'id': 1, 'docs': [1], 'type': 'history'},
+    'trash': {'name': 'Trash', 'id': 3, 'docs': [2]},
+    'mine': [
+      {'name': 'default', 'id': 2, 'docs': [3]},
+      {'name': 'web', 'id': 3, 'docs': [3]}
+    ],
+    'notmine': [
+      {'name': 'romain', 'projects': [
+        {'name': 'example', 'id': 20, 'docs': [10]},
+        {'name': 'ex2', 'id': 30, 'docs': [10, 11]}
+      ]},
+      {'name': 'pai', 'projects': [
+        {'name': 'example2', 'id': 20, 'docs': [10]}
+      ]}
+    ]
+  };
+
+  var ALL_DOCUMENTS = json_docs;
+  self.tags = ko.mapping.fromJS(json_tags);
+  self.documents = ko.observableArray([]);
+
+  self.editTagsToCreate = ko.observableArray([]);
+  self.editTagsToDelete = ko.observableArray([]);
+
+  self.selectedTag = ko.observable("");
+
+  self.trash = ko.computed(function () {
+    return self.tags.trash;
+  });
+
+  self.history = ko.computed(function () {
+    return self.tags.history;
+  });
+}

+ 177 - 0
apps/search/static/js/template.ko.js

@@ -0,0 +1,177 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+var Query = function (importable) {
+  var self = this;
+  self.type = ko.observable(importable.type);
+  self.name = ko.observable(importable.name);
+  self.selected = ko.observable(false);
+  self.handleSelect = function (row, e) {
+    this.selected(!this.selected());
+  };
+};
+
+var Collection = function (coll) {
+  var self = this;
+
+  self.id = ko.observable(coll.id);
+  self.name = ko.observable(coll.name);
+  self.label = ko.observable(coll.label);
+  self.isCoreOnly = ko.observable(coll.isCoreOnly);
+  self.absoluteUrl = ko.observable(coll.absoluteUrl);
+  self.selected = ko.observable(false);
+  self.hovered = ko.observable(false);
+
+  self.handleSelect = function (row, e) {
+    this.selected(!this.selected());
+  };
+  self.toggleHover = function (row, e) {
+    this.hovered(!this.hovered());
+  };
+};
+
+// sorting
+// highlithing
+
+//
+// Facets (text or chart)
+//   field 
+//   range
+
+// spacial search
+// query facet
+// pivot facet
+
+
+
+var SearchViewModel = function (result, facets) {
+  var self = this;
+
+  // Mock testing
+  //var TEMPLATE = {"extracode": "      \n<style type=\"text/css\">\nem {\n  font-weight: bold;\n  background-color: yellow;\n}\n.avatar {\n  margin: 10px;\n}\n.created {\n  margin-top: 10px;\n  color: #CCC;\n}\n.openTweet {\n  float: right;\n  margin: 10px;\n}\n</style>\n      \n    ", "highlighting": ["text"], "properties": {"highlighting_enabled": true}, "template": "\n\n<div class=\"row-fluid\">\n  <div class=\"row-fluid\">\n    <div class=\"row-fluid\">\n      <div class=\"span1\">\n        <img src=\"http://twitter.com/api/users/profile_image/{{user_screen_name}}\" class=\"avatar\">\n        </div>\n        <div class=\"span11\">\n          <b>{{user_name}}</b>\n          <br>\n            <a href=\"https://twitter.com/{{user_screen_name}}/status/{{id}}\" target=\"_blank\">\n              {{text}}\n            </a>\n            <br>\n              <div class=\"created\">{{#fromnow}}{{created_at}}{{/fromnow}}</div>\n            </div>\n          </div>\n          <br>\n          </div>\n        </div>\n        \n        "}
+  //var FACETS = {"dates": [], "fields": [{"uuid": "f6618a5c-bbba-2886-1886-bbcaf01409ca", "verbatim": "", "isVerbatim": false, "label": "Location", "field": "user_location", "type": "field"}], "charts": [{"end": "2014-02-28T12:00:00Z", "uuid": "4883871c-0cea-8547-de60-7166d498098a", "verbatim": "", "start": "2014-02-28T12:00:00Z-10DAYS", "isVerbatim": false, "label": "Posted", "field": "created_at", "gap": "+5MINUTES", "type": "chart"}], "properties": {"sort": "count", "mincount": 1, "isEnabled": true, "limit": 10}, "ranges": [{"end": "1000", "uuid": "5533165a-0b1c-21b6-4ede-9d2fc301ed6b", "verbatim": "", "start": 0, "isVerbatim": false, "label": "Followers count", "field": "user_followers_count", "gap": "100", "type": "range"}, {"end": "10000", "uuid": "d5e66f3d-ca7d-67d7-05c7-33ec499cc106", "verbatim": "", "start": 0, "isVerbatim": false, "label": "Tweet count", "field": "user_statuses_count", "gap": "1000", "type": "range"}], "order": ["f6618a5c-bbba-2886-1886-bbcaf01409ca", "5533165a-0b1c-21b6-4ede-9d2fc301ed6b", "d5e66f3d-ca7d-67d7-05c7-33ec499cc106"]}
+  
+  var TEMPLATE = {"extracode": "", "highlighting": ["text"], "properties": {"highlighting_enabled": true},
+		          "template": "{{user_screen_name}} {{user_name}} {{text}}", "isGridLayout": true, "fields": ["user_screen_name", "user_name", "text"]
+  };
+  var FACETS = {"dates": [], "fields": [
+                   {
+                	   "uuid": "f6618a5c-bbba-2886-1886-bbcaf01409ca", "verbatim": "", "isVerbatim": false, "label": "Location", 
+                	    "field": "user_location", "type": "field"
+                   }
+                 ],
+                 "charts": [], "properties": {"sort": "count", "mincount": 1, "isEnabled": true, "limit": 10}, "ranges": [], "order": []
+  };  
+
+  
+  // Collection customization
+  var collection = 10000004;
+  self.template = ko.mapping.fromJS(TEMPLATE); //result.template;
+  self.template.fields.subscribe(function() {
+	self.search();
+  });
+  self.template.template.subscribe(function() {
+    self.search();
+  });
+  self.facets = ko.mapping.fromJS(FACETS.fields); //facets.fields
+
+  self.fields = ko.observableArray(["user_screen_name", "user_name", "text", "created_at", "user_statuses_count", "id"]); // ad dynamic ajaxifoed
+  
+  // Query URL  
+  self.q = ko.observable('');
+  self.qFacets = {}
+  
+  // Query results
+  self.response = ko.observable({});
+  self.results = ko.observableArray([]);
+  self.norm_facets = ko.computed(function () {
+    return self.response().normalized_facets;
+  });
+  
+  // Forms
+  self.selectedFacet = ko.observable();
+  
+  self.search = function () {
+    $.post("/search/query2?format=json", {
+        collection: collection,
+        q: ko.toJSON(self.q),
+        facets: ko.toJSON(self.facets),
+        fq: ko.utils.stringifyJson(self.qFacets),
+        template:  ko.mapping.toJSON(self.template),
+      }, function (data) {
+       self.response(data);
+   	   self.results.removeAll(); 
+   	   if (data.error) {
+   		 $(document).trigger("error", data.error);   
+   	   } else {
+   	     if (self.template.isGridLayout()) {
+ 	       // Table view
+ 	       $.each(data.response.docs, function (index, item) {
+ 	    	 var row = [];
+ 	    	 $.each(self.template.fields(), function (index, column) {
+ 	    	   row.push(item[column]); // todo if is null
+ 	    	 });
+ 	    	 self.results.push(row);
+ 	       });
+   	     } else {
+   	   	   // Template view
+   	       var _mustacheTmpl = fixTemplateDotsAndFunctionNames(self.template.template());
+           $.each(data.response.docs, function (index, item) {
+             addTemplateFunctions(item);
+             self.results.push(Mustache.render(_mustacheTmpl, item));
+           });
+         }
+   	   }
+     }).fail(function(xhr, textStatus, errorThrown) {    	
+       $(document).trigger("error", xhr.responseText); // cleanup all "alert jHueNotify alert-error" before
+     });
+  };
+  
+  self.addFacet = function(facet_json) {
+    self.facets.push(ko.mapping.fromJS({
+	   "uuid": "f6618a5c-bbba-2886-1886-bbcaf01409ca", "verbatim": "", "isVerbatim": false, "label": "Location", 
+	    "field": facet_json.name, "type": "field"
+    }));
+  }
+  
+  self.selectSingleFacet = function(normalized_facet_json) {
+	$.each(self.facets(), function(index, facet) {
+      if (facet.field() == normalized_facet_json.field) {
+        self.selectedFacet(facet);
+      }
+	});	  
+  }
+  
+  self.removeFacet = function(facet_json) {
+	 $.each(self.facets(), function(index, item) {
+		if (item.field() == facet_json.field) {
+		  self.facets.remove(item);
+		}
+	 });
+	 self.search();
+  }
+  
+  self.selectFacet = function(facet_json) {
+	self.qFacets[facet_json.cat] = facet_json.value;
+	self.search();
+  }
+
+  self.unselectFacet = function(facet_json) {
+	delete self.qFacets[facet_json.cat];
+    self.search();
+  }
+};

+ 14 - 3
desktop/core/src/desktop/templates/common_header.mako

@@ -324,7 +324,18 @@ from django.utils.translation import ugettext as _
     <a class="brand nav-tooltip pull-left" title="${_('About Hue')}" rel="navigator-tooltip" href="/about"><img src="/static/art/hue-logo-mini-white.png" data-orig="/static/art/hue-logo-mini-white.png" data-hover="/static/art/hue-logo-mini-white-hover.png"/></a>
     % if user.is_authenticated() and section != 'login':
      <ul class="nav nav-pills pull-left">
-       <li><a title="${_('My documents')}" rel="navigator-tooltip" href="${ url('desktop.views.home') }" style="padding-bottom:2px!important"><i class="fa fa-home" style="font-size: 19px"></i></a></li>
+       <li class="dropdown">
+         <a title="${_('My documents')}" rel="navigator-tooltip" href="#" data-toggle="dropdown" class="dropdown-toggle" href="${ url('desktop.views.home') }" style="padding-bottom:2px!important">
+           <i class="fa fa-home" style="font-size: 19px"></i>
+           Home
+           <b class="caret"></b>
+         </a>       
+         <ul role="menu" class="dropdown-menu">
+           <li><a href="${ url('desktop.views.home') }"><i class="fa fa-file"></i> ${_('Documents')}</a></li>
+           <li><a href="${ url('oozie:index') }"><img src="/oozie/static/art/icon_oozie_dashboard_24.png" /> ${_('Dashboards')}</a></li>
+           <li><a href="${ url('oozie:list_workflows') }"><img src="/oozie/static/art/icon_oozie_editor_24.png" /> ${_('Reports')}</a></li>
+         </ul>
+       </li>         
        <%
          query_apps = count_apps(apps, ['beeswax', 'impala', 'rdbms', 'pig', 'jobsub', 'spark']);
        %>
@@ -393,7 +404,7 @@ from django.utils.translation import ugettext as _
          <% collections = SearchController(user).get_search_collections() %>
          % if not collections:
            <li>
-             <a title="${_('Solr Search')}" rel="navigator-tooltip" href="${ url('search:index') }">${_('Search')}</a>
+             <a title="${_('Solr Search')}" rel="navigator-tooltip" href="${ url('search:index') }">Search</a>
            </li>
          % else:
            <li class="dropdown">
@@ -405,7 +416,7 @@ from django.utils.translation import ugettext as _
              </ul>
            </li>
          % endif
-       % endif
+       % endif     
        % if other_apps:
        <li class="dropdown">
          <a href="#" data-toggle="dropdown" class="dropdown-toggle">${_('Other apps')} <b class="caret"></b></a>