diff --git a/haystack/admin.py b/haystack/admin.py
index 3390a9dd0..0e295f369 100644
--- a/haystack/admin.py
+++ b/haystack/admin.py
@@ -1,6 +1,5 @@
from django.contrib.admin.options import ModelAdmin
-from django.contrib.admin.views.main import (ChangeList, MAX_SHOW_ALL_ALLOWED,
- SEARCH_VAR)
+from django.contrib.admin.views.main import ChangeList, SEARCH_VAR
from django.core.exceptions import PermissionDenied, ImproperlyConfigured
from django.core.paginator import Paginator, InvalidPage
from django.shortcuts import render_to_response
@@ -22,6 +21,10 @@ def wraps(request, *args, **kwargs):
csrf_protect_m = method_decorator(csrf_protect)
+try:
+ from django.contrib.admin.views.main import MAX_SHOW_ALL_ALLOWED
+except ImportError:
+ pass
class SearchChangeList(ChangeList):
def get_results(self, request):
@@ -36,7 +39,8 @@ def get_results(self, request):
result_count = paginator.count
full_result_count = SearchQuerySet().models(self.model).all().count()
- can_show_all = result_count <= MAX_SHOW_ALL_ALLOWED
+ can_show_all = result_count <= getattr(self, 'list_max_show_all', MAX_SHOW_ALL_ALLOWED)
+
multi_page = result_count > self.list_per_page
# Get the list of objects to display on this page.
diff --git a/haystack/backends/__init__.py b/haystack/backends/__init__.py
index ac2a34100..d4cfd2bec 100644
--- a/haystack/backends/__init__.py
+++ b/haystack/backends/__init__.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+import inspect
from copy import deepcopy
from time import time
from django.conf import settings
@@ -22,7 +23,6 @@
# A means to inspect all search queries that have run in the last request.
queries = []
-
# Per-request, reset the ghetto query log.
# Probably not extraordinarily thread-safe but should only matter when
# DEBUG = True.
@@ -55,6 +55,7 @@ def wrapper(obj, query_string, *args, **kwargs):
'additional_args': args,
'additional_kwargs': kwargs,
'time': "%.3f" % (stop - start),
+ 'stacktrace': inspect.stack()
})
return wrapper
@@ -122,7 +123,7 @@ def clear(self, models=[], commit=True):
@log_query
def search(self, query_string, sort_by=None, start_offset=0, end_offset=None,
fields='', highlight=False, facets=None, date_facets=None, query_facets=None,
- narrow_queries=None, spelling_query=None,
+ pivot_facets=None, narrow_queries=None, spelling_query=None,
limit_to_registered_models=None, result_class=None, **kwargs):
"""
Takes a query to search on and returns dictionary.
@@ -280,6 +281,7 @@ def __init__(self, site=None, backend=None):
self.order_by = []
self.models = set()
self.boost = {}
+ self.dismax = {}
self.start_offset = 0
self.end_offset = None
self.highlight = False
@@ -287,8 +289,12 @@ def __init__(self, site=None, backend=None):
self.date_facets = {}
self.facet_mincount = None
self.facet_limit = None
+ self.facet_field_limit = {}
self.facet_prefix = None
+ self.facet_sort = None
self.query_facets = []
+ self.pivot_facets = set()
+ self.facet_pivot_mincount = None
self.narrow_queries = set()
self._raw_query = None
self._raw_query_params = {}
@@ -357,15 +363,27 @@ def build_params(self, spelling_query=None):
if self.query_facets:
kwargs['query_facets'] = self.query_facets
+
+ if self.pivot_facets:
+ kwargs['pivot_facets'] = list(self.pivot_facets)
if self.facet_mincount:
kwargs['facet_mincount'] = self.facet_mincount
if self.facet_limit:
kwargs['facet_limit'] = self.facet_limit
+
+ if self.facet_field_limit:
+ kwargs['facet_field_limit'] = self.facet_field_limit
if self.facet_prefix:
kwargs['facet_prefix'] = self.facet_prefix
+
+ if self.facet_sort:
+ kwargs['facet_sort'] = self.facet_sort
+
+ if self.facet_pivot_mincount:
+ kwargs['facet_pivot_mincount'] = self.facet_pivot_mincount
if self.narrow_queries:
kwargs['narrow_queries'] = self.narrow_queries
@@ -604,14 +622,24 @@ def add_order_by(self, field):
"""Orders the search result by a field."""
self.order_by.append(field)
- def set_facet_mincount(self,mincount):
+ def set_facet_mincount(self, mincount):
self.facet_mincount = mincount
def set_facet_limit(self, limit):
self.facet_limit = limit
+
+ def set_facet_field_limit(self, field, limit):
+ facet_field = self.backend.site.get_facet_field_name(field)
+ self.facet_field_limit[facet_field] = limit
- def set_facet_prefix(self,prefix):
+ def set_facet_prefix(self, prefix):
self.facet_prefix = prefix
+
+ def set_facet_sort(self, sort):
+ self.facet_sort = sort
+
+ def set_facet_pivot_mincount(self, mincount):
+ self.facet_pivot_mincount = mincount
def clear_order_by(self):
"""
@@ -660,7 +688,15 @@ def raw_search(self, query_string, **kwargs):
"""
self._raw_query = query_string
self._raw_query_params = kwargs
-
+
+ def add_dismax(self, **kwargs):
+ """
+ Allows backends with support for "dismax" to perform enhanced user query parsing
+ """
+
+ self.dismax = dict([(p, v) for p, v in kwargs.iteritems()
+ if p in getattr(self.backend, 'DISMAX_PARAMETERS', [])])
+
def more_like_this(self, model_instance):
"""
Allows backends with support for "More Like This" to return results
@@ -693,6 +729,13 @@ def add_date_facet(self, field, start_date, end_date, gap_by, gap_amount=1):
def add_query_facet(self, field, query):
"""Adds a query facet on a field."""
self.query_facets.append((self.backend.site.get_facet_field_name(field), query))
+
+ def add_pivot_facet(self, *args):
+ """Adds pivot faceting to a query for the provided fields."""
+ facet_fields = []
+ for field in args:
+ facet_fields.append(self.backend.site.get_facet_field_name(field))
+ self.pivot_facets.add((','.join(facet_fields)))
def add_narrow_query(self, query):
"""
@@ -752,13 +795,18 @@ def _clone(self, klass=None):
clone.order_by = self.order_by[:]
clone.models = self.models.copy()
clone.boost = self.boost.copy()
+ clone.dismax = self.dismax.copy()
clone.highlight = self.highlight
clone.facets = self.facets.copy()
clone.date_facets = self.date_facets.copy()
clone.query_facets = self.query_facets[:]
+ clone.pivot_facets = self.pivot_facets.copy()
clone.facet_mincount = self.facet_mincount
clone.facet_limit = self.facet_limit
+ clone.facet_field_limit = self.facet_field_limit.copy()
clone.facet_prefix = self.facet_prefix
+ clone.facet_sort = self.facet_sort
+ clone.facet_pivot_mincount = self.facet_pivot_mincount
clone.narrow_queries = self.narrow_queries.copy()
clone.start_offset = self.start_offset
clone.end_offset = self.end_offset
@@ -766,4 +814,5 @@ def _clone(self, klass=None):
clone.result_class = self.result_class
clone._raw_query = self._raw_query
clone._raw_query_params = self._raw_query_params
+ clone._facet_counts = self._facet_counts
return clone
diff --git a/haystack/backends/solr_backend.py b/haystack/backends/solr_backend.py
index 75ddc18fb..ce4eb9be8 100644
--- a/haystack/backends/solr_backend.py
+++ b/haystack/backends/solr_backend.py
@@ -38,14 +38,29 @@ class SearchBackend(BaseSearchBackend):
'[', ']', '^', '"', '~', '*', '?', ':',
)
+ DISMAX_PARAMETERS = (
+ 'defType',
+ 'qf',
+ 'q.alt',
+ 'mm',
+ 'pf',
+ 'ps',
+ 'qs',
+ 'tie',
+ 'bq',
+ 'bf',
+ 'boost',
+ )
+
def __init__(self, site=None):
super(SearchBackend, self).__init__(site)
- if not hasattr(settings, 'HAYSTACK_SOLR_URL'):
- raise ImproperlyConfigured('You must specify a HAYSTACK_SOLR_URL in your settings.')
+ if not hasattr(settings, 'HAYSTACK_SOLR_URLS'):
+ raise ImproperlyConfigured('You must specify a HAYSTACK_SOLR_URLS in your settings.')
timeout = getattr(settings, 'HAYSTACK_SOLR_TIMEOUT', 10)
- self.conn = Solr(settings.HAYSTACK_SOLR_URL, timeout=timeout)
+ self.conn = Solr(settings.HAYSTACK_SOLR_URLS['MASTER'], timeout=timeout)
+ self.conn_slave = Solr(settings.HAYSTACK_SOLR_URLS['SLAVE'], timeout=timeout)
self.log = logging.getLogger('haystack')
def update(self, index, iterable, commit=True):
@@ -99,8 +114,10 @@ def clear(self, models=[], commit=True):
@log_query
def search(self, query_string, sort_by=None, start_offset=0, end_offset=None,
fields='', highlight=False, facets=None, date_facets=None, query_facets=None,
- narrow_queries=None, spelling_query=None, facet_mincount=None, facet_limit=None, facet_prefix=None,
+ pivot_facets=None, narrow_queries=None, spelling_query=None, facet_mincount=None, facet_limit=None,
+ facet_field_limit=None, facet_prefix=None, facet_sort=None, facet_pivot_mincount=None, dismax=None,
limit_to_registered_models=None, result_class=None, **kwargs):
+
if len(query_string) == 0:
return {
'results': [],
@@ -146,10 +163,19 @@ def search(self, query_string, sort_by=None, start_offset=0, end_offset=None,
if facet_limit is not None:
kwargs['facet'] = 'on'
kwargs['facet.limit'] = facet_limit
+
+ if facet_field_limit is not None:
+ kwargs['facet'] = 'on'
+ for f, limit in facet_field_limit.iteritems():
+ kwargs['f.%s.facet.limit' % f] = limit
if facet_prefix is not None:
kwargs['facet'] = 'on'
kwargs['facet.prefix'] = facet_prefix
+
+ if facet_sort is not None:
+ kwargs['facet'] = 'on'
+ kwargs['facet.sort'] = facet_sort
if date_facets is not None:
kwargs['facet'] = 'on'
@@ -157,8 +183,8 @@ def search(self, query_string, sort_by=None, start_offset=0, end_offset=None,
kwargs['facet.date.other'] = 'none'
for key, value in date_facets.items():
- kwargs["f.%s.facet.date.start" % key] = self.conn._from_python(value.get('start_date'))
- kwargs["f.%s.facet.date.end" % key] = self.conn._from_python(value.get('end_date'))
+ kwargs["f.%s.facet.date.start" % key] = self.conn_slave._from_python(value.get('start_date'))
+ kwargs["f.%s.facet.date.end" % key] = self.conn_slave._from_python(value.get('end_date'))
gap_by_string = value.get('gap_by').upper()
gap_string = "%d%s" % (value.get('gap_amount'), gap_by_string)
@@ -170,6 +196,14 @@ def search(self, query_string, sort_by=None, start_offset=0, end_offset=None,
if query_facets is not None:
kwargs['facet'] = 'on'
kwargs['facet.query'] = ["%s:%s" % (field, value) for field, value in query_facets]
+
+ if pivot_facets is not None:
+ kwargs['facet'] = 'on'
+ kwargs['facet.pivot'] = pivot_facets
+
+ if facet_pivot_mincount is not None:
+ kwargs['facet'] = 'on'
+ kwargs['facet.pivot_mincount'] = facet_pivot_mincount
if limit_to_registered_models is None:
limit_to_registered_models = getattr(settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
@@ -187,9 +221,12 @@ def search(self, query_string, sort_by=None, start_offset=0, end_offset=None,
if narrow_queries is not None:
kwargs['fq'] = list(narrow_queries)
+
+ if dismax is not None:
+ kwargs.update(dismax)
try:
- raw_results = self.conn.search(query_string, **kwargs)
+ raw_results = self.conn_slave.search(query_string, **kwargs)
except (IOError, SolrError), e:
self.log.error("Failed to query Solr using '%s': %s", query_string, e)
raw_results = EmptyResults()
@@ -242,7 +279,7 @@ def more_like_this(self, model_instance, additional_query_string=None,
query = "%s:%s" % (ID, get_identifier(model_instance))
try:
- raw_results = self.conn.more_like_this(query, field_name, **params)
+ raw_results = self.conn_slave.more_like_this(query, field_name, **params)
except (IOError, SolrError), e:
self.log.error("Failed to fetch More Like This from Solr for document '%s': %s", query, e)
raw_results = EmptyResults()
@@ -262,12 +299,13 @@ def _process_results(self, raw_results, highlight=False, result_class=None):
if result_class is None:
result_class = SearchResult
-
+
if hasattr(raw_results, 'facets'):
facets = {
'fields': raw_results.facets.get('facet_fields', {}),
'dates': raw_results.facets.get('facet_dates', {}),
'queries': raw_results.facets.get('facet_queries', {}),
+ 'pivots': raw_results.facets.get('facet_pivot', {}),
}
for key in ['fields']:
@@ -275,7 +313,22 @@ def _process_results(self, raw_results, highlight=False, result_class=None):
# Convert to a two-tuple, as Solr's json format returns a list of
# pairs.
facets[key][facet_field] = zip(facets[key][facet_field][::2], facets[key][facet_field][1::2])
-
+
+ def _process_pivot(pivot):
+ facet = []
+ for p in pivot:
+ if not 'pivot' in p:
+ facet.append((unicode(p['value']), p['count'], ()))
+ else:
+ facet.append((unicode(p['value']), p['count'], tuple(_process_pivot(p['pivot']))))
+
+ return facet
+
+ for key in ['pivots']:
+ for facet_pivot in facets[key]:
+ # Convert to a three-tuple, with pairs + nested pivot
+ facets[key][facet_pivot] = _process_pivot(facets[key][facet_pivot])
+
if getattr(settings, 'HAYSTACK_INCLUDE_SPELLING', False) is True:
if hasattr(raw_results, 'spellcheck'):
if len(raw_results.spellcheck.get('suggestions', [])):
@@ -298,7 +351,7 @@ def _process_results(self, raw_results, highlight=False, result_class=None):
if string_key in index.fields and hasattr(index.fields[string_key], 'convert'):
additional_fields[string_key] = index.fields[string_key].convert(value)
else:
- additional_fields[string_key] = self.conn._to_python(value)
+ additional_fields[string_key] = self.conn_slave._to_python(value)
for name in [DJANGO_CT, DJANGO_ID, 'score']:
if name in additional_fields:
@@ -398,7 +451,7 @@ def build_query_fragment(self, field, filter_type, value):
if not isinstance(value, (list, tuple)):
# Convert whatever we find to what pysolr wants.
- value = self.backend.conn._from_python(value)
+ value = self.backend.conn_slave._from_python(value)
# Check to see if it's a phrase for an exact match.
if ' ' in value:
@@ -424,7 +477,7 @@ def build_query_fragment(self, field, filter_type, value):
in_options = []
for possible_value in value:
- in_options.append('%s:"%s"' % (index_fieldname, self.backend.conn._from_python(possible_value)))
+ in_options.append('%s:"%s"' % (index_fieldname, self.backend.conn_slave._from_python(possible_value)))
result = "(%s)" % " OR ".join(in_options)
elif filter_type == 'range':
@@ -436,6 +489,27 @@ def build_query_fragment(self, field, filter_type, value):
return result
+ def build_facet_field(self, facet, key, ex):
+ f_mask = ''
+ tok = []
+ if key or ex:
+ f_mask += '{!'
+ if key:
+ f_mask += 'key=%s'
+ tok.append(key)
+ if ex:
+ if key:
+ f_mask += ' '
+ f_mask += 'ex=%s'
+ tok.append(ex)
+ if key or ex:
+ f_mask += '}'
+ f_mask += facet
+ if len(tok) > 0:
+ facet = f_mask % tuple(tok)
+
+ return facet
+
def run(self, spelling_query=None):
"""Builds and executes the query. Returns a list of search results."""
final_query = self.build_query()
@@ -460,9 +534,13 @@ def run(self, spelling_query=None):
if self.highlight:
kwargs['highlight'] = self.highlight
-
+
if self.facets:
- kwargs['facets'] = list(self.facets)
+ facets = []
+ for facet, key, ex in list(self.facets):
+ if facet:
+ facets.append(self.build_facet_field(facet, key, ex))
+ kwargs['facets'] = facets
if self.date_facets:
kwargs['date_facets'] = self.date_facets
@@ -473,14 +551,39 @@ def run(self, spelling_query=None):
if self.facet_limit:
kwargs['facet_limit'] = self.facet_limit
+ if self.facet_field_limit:
+ kwargs['facet_field_limit'] = self.facet_field_limit
+
if self.facet_prefix:
kwargs['facet_prefix'] = self.facet_prefix
+
+ if self.facet_sort:
+ kwargs['facet_sort'] = self.facet_sort
+
+ if self.facet_pivot_mincount:
+ kwargs['facet_pivot_mincount'] = self.facet_pivot_mincount
if self.query_facets:
kwargs['query_facets'] = self.query_facets
+
+ if self.pivot_facets:
+ facets = []
+ for facet, key, ex in list(self.pivot_facets):
+ if facet:
+ facets.append(self.build_facet_field(facet, key, ex))
+ kwargs['pivot_facets'] = facets
if self.narrow_queries:
- kwargs['narrow_queries'] = self.narrow_queries
+ narrow_queries = []
+ for query, tag in list(self.narrow_queries):
+ if query and tag:
+ query = '{!tag=%s}' % tag + query
+ narrow_queries.append(query)
+
+ kwargs['narrow_queries'] = narrow_queries
+
+ if self.dismax:
+ kwargs['dismax'] = self.dismax
if spelling_query:
kwargs['spelling_query'] = spelling_query
@@ -508,3 +611,56 @@ def run_mlt(self):
results = self.backend.more_like_this(self._mlt_instance, additional_query_string, **kwargs)
self._results = results.get('results', [])
self._hit_count = results.get('hits', 0)
+
+ def add_field_facet(self, field, key=None, ex=[]):
+ """Adds a regular facet on a field."""
+ facet_field = self.backend.site.get_facet_field_name(field)
+ self.facets.add((facet_field, key, ','.join(ex)))
+
+ def add_narrow_query(self, query, tag=[]):
+ """
+ Narrows a search to a subset of all documents per the query.
+
+ Generally used in conjunction with faceting.
+ """
+ self.narrow_queries.add((query, ','.join(tag)))
+
+ def add_pivot_facet(self, *args, **kwargs):
+ """Adds pivot faceting to a query for the provided fields."""
+ key = kwargs.get('key', None)
+ ex = kwargs.get('ex', [])
+ facet_fields = []
+ for field in args:
+ facet_fields.append(self.backend.site.get_facet_field_name(field))
+ self.pivot_facets.add((','.join(facet_fields), key, ','.join(ex)))
+
+ def post_process_facets(self, results):
+ # Handle renaming the facet fields. Undecorate and all that.
+ revised_facets = {}
+ field_data = self.backend.site.all_searchfields()
+
+ for facet_type, field_details in results.get('facets', {}).items():
+ temp_facets = {}
+
+ for field, field_facets in field_details.items():
+ fieldname = []
+
+ if facet_type in ['pivots']:
+ field = field.split(',')
+ else:
+ field = [field]
+
+ for f in field:
+ if f in field_data and hasattr(field_data[f], 'get_facet_for_name'):
+ fieldname.append(field_data[f].get_facet_for_name())
+
+ if len(fieldname) == 0:
+ fieldname = field[0]
+ else:
+ fieldname = ','.join(fieldname)
+
+ temp_facets[fieldname] = field_facets
+
+ revised_facets[facet_type] = temp_facets
+
+ return revised_facets
diff --git a/haystack/query.py b/haystack/query.py
index 52e10d1be..400574b50 100644
--- a/haystack/query.py
+++ b/haystack/query.py
@@ -321,10 +321,10 @@ def boost(self, term, boost):
clone.query.add_boost(term, boost)
return clone
- def facet(self, field):
+ def facet(self, field, **kwargs):
"""Adds faceting to a query for the provided field."""
clone = self._clone()
- clone.query.add_field_facet(field)
+ clone.query.add_field_facet(field, **kwargs)
return clone
def date_facet(self, field, start_date, end_date, gap_by, gap_amount=1):
@@ -338,29 +338,53 @@ def query_facet(self, field, query):
clone = self._clone()
clone.query.add_query_facet(field, query)
return clone
+
+ def pivot_facet(self, *args, **kwargs):
+ """Adds pivot faceting to a query for the provided fields."""
+ clone = self._clone()
+ clone.query.add_pivot_facet(*args, **kwargs)
+ return clone
- def facet_mincount(self,mincount):
+ def facet_mincount(self, mincount):
"""Sets mincount for facet result"""
clone = self._clone()
clone.query.set_facet_mincount(mincount)
return clone
- def facet_limit(self,limit):
+ def facet_limit(self, limit):
"""Sets limit for facet result"""
clone = self._clone()
clone.query.set_facet_limit(limit)
return clone
+
+ def facet_field_limit(self, field, limit):
+ """Sets limit for facet result on given field"""
+ clone = self._clone()
+ clone.query.set_facet_field_limit(field, limit)
+ return clone
def facet_prefix(self,prefix):
"""Sets prefix for facet result"""
clone = self._clone()
clone.query.set_facet_prefix(prefix)
return clone
+
+ def facet_sort(self, sort):
+ """Sets order of facet result"""
+ clone = self._clone()
+ clone.query.set_facet_sort(sort)
+ return clone
+
+ def facet_pivot_mincount(self, mincount):
+ """Sets mincount for pivot facet result"""
+ clone = self._clone()
+ clone.query.set_facet_pivot_mincount(mincount)
+ return clone
- def narrow(self, query):
+ def narrow(self, query, **kwargs):
"""Pushes existing facet choices into the search."""
clone = self._clone()
- clone.query.add_narrow_query(query)
+ clone.query.add_narrow_query(query, **kwargs)
return clone
def raw_search(self, query_string, **kwargs):
@@ -369,6 +393,12 @@ def raw_search(self, query_string, **kwargs):
clone.query.raw_search(query_string, **kwargs)
return clone
+ def dismax(self, **kwargs):
+ """Sets dismax parameters for capable backends"""
+ clone = self._clone()
+ clone.query.add_dismax(**kwargs)
+ return clone
+
def load_all(self):
"""Efficiently populates the objects in the search results."""
clone = self._clone()
diff --git a/haystack/utils/debugtoolbar.py b/haystack/utils/debugtoolbar.py
new file mode 100644
index 000000000..487520290
--- /dev/null
+++ b/haystack/utils/debugtoolbar.py
@@ -0,0 +1,59 @@
+import django
+from django.utils.translation import ugettext_lazy as _
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+from debug_toolbar.panels import DebugPanel
+from debug_toolbar.utils import tidy_stacktrace
+
+import haystack.backends
+
+class HaystackDebugPanel(DebugPanel):
+ """
+ Panel that displays the Haystack queries.
+ """
+ name = 'Haystack'
+ template = 'debug_toolbar/panels/sql.html'
+ has_content = True
+
+ def __init__(self, *args, **kwargs):
+ super(HaystackDebugPanel, self).__init__(*args, **kwargs)
+ self._queries = haystack.backends.queries
+
+ def nav_title(self):
+ return _('Haystack queries')
+
+ def nav_subtitle(self):
+ return "%s queries" % len(self._queries)
+
+ def url(self):
+ return ''
+
+ def title(self):
+ return 'Haystack Queries'
+
+ def _transform_row(self, row):
+ data = {
+ 'sql': unicode(row['query_string']) + unicode(row['additional_args']) + unicode(row['additional_kwargs']),
+ 'duration': row['time'],
+ 'width_ratio_relative': 0,
+ 'start_offset': 0
+ }
+ stacktrace = []
+ if 'stacktrace' in row:
+ row['stacktrace'] = reversed(tidy_stacktrace(row['stacktrace']))
+ for frame in row['stacktrace']:
+ params = map(escape, frame[0].rsplit('/', 1) + list(frame[1:]))
+ try:
+ stacktrace.append(u'{0}/{1} in {3}({2})\n {4}'.format(*params))
+ except IndexError:
+ # This frame doesn't have the expected format, so skip it and move on to the next one
+ continue
+ data['stacktrace'] = mark_safe('\n'.join(stacktrace))
+ return data
+
+ def process_response(self, request, response):
+ self.record_stats({
+ 'databases': [],
+ 'queries': [self._transform_row(q) for q in self._queries],
+ 'duration': 0,
+ })