from zope.component import getUtility
from zeit.retresco.interfaces import IElasticsearch


builders = {}


def search(query, sort_order=None, additional_result_fields=(), rows=50, **kw):
    """Search elasticsearch according to query."""
    if query is None:
        return []
    sort_order = '_score'
    elasticsearch = getUtility(IElasticsearch)
    return elasticsearch.search(query, sort_order=sort_order, rows=rows, **kw)


def builder(func):
    builders[func.__name__] = func
    return func


@builder
def fulltext(conditions):
    value = conditions['fulltext']
    return 'must', [dict(query_string=dict(query=value))]


@builder
def from_(conditions):
    filters = dict()
    if 'from_' in conditions:
        filters['gte'] = conditions['from_'].isoformat()
    if 'until' in conditions:
        filters['lte'] = conditions['until'].isoformat()
    return 'must', [dict(range={
        'payload.document.last-semantic-change': filters})]


@builder
def until(conditions):
    if 'from_' not in conditions:
        return from_(conditions)
    return None, None


@builder
def show_news(conditions):
    if not conditions['show_news']:
        return 'must_not', [{'payload.document.ressort': 'News'}] + [
            {'payload.workflow.product-id': pid}
            for pid in 'News', 'afp', 'SID', 'dpa-hamburg']
    return None, None


field_map = dict(
    authors='payload.document.author',
    keywords='rtr_keywords',
    product_id='payload.workflow.product-id',
    published='payload.vivi.publish_status',
    raw_tags='rtr_tags',
    serie='payload.document.serie',
    topic='payload.document.ressort',
    types='payload.document.type',
    volume='payload.document.volume',
    year='payload.document.year',
)


def query(**kw):
    """ Create elasticsearch query for search. Supported field are:

    fulltext - fulltext to search for
    from_ - search only after from_ datetime. If None, no start to range.
    until - search only until until_ datetime. If None, no end to range.
    volume - volume in which to search. If None, no restriction.
    year - year in which to search (related to volume). If None,
           no restriction.
    topic - the topic to look for. If None, no topic restriction.
    authors - parts of the author's name to look for, whitespace separated.
              If None, no author restriction.
    keywords - keywords to look for, whitespace separated.
              If None, no keyword restriction.
    raw_tags - whole raw-tags content as escaped xml data.
    published - Publication status to look for. If None, no restriction.
    types - a list of document types to look for. If None types don't matter.
    show_news - Display ticker messages or not (boolean)
    filter_terms - an optional list of extra terms to add to the filter.
                   If None, no extra terms.

    Returns elasticsearch query expression that can be passed to `search`.
    """
    clauses = dict()
    for field, value in kw.items():
        if value in (None, [], ()):
            continue
        elif field in builders:
            typ, clause = builders[field](kw)
        elif field in field_map:
            typ, clause = 'must', [dict(match={field_map[field]: value})]
        else:
            raise ValueError('unsupported search condition {}', field)
        if clause is not None:
            clauses.setdefault(typ, []).extend(clause)
    if len(clauses) == 1 and len(clauses.get('must', [])) == 1:
        qry = clauses['must'][0]
    elif clauses:
        qry = dict(bool=clauses)
    else:
        qry = dict(match_all=dict())
    return dict(query=qry)

@@ -89,24 +89,8 @@
 h%0A%0A%0A
-builders = %7B%7D%0A%0A%0A
 def 
@@ -414,930 +414,8 @@
 )%0A%0A%0A
-def builder(func):%0A    builders%5Bfunc.__name__%5D = func%0A    return func%0A%0A%0A@builder%0Adef fulltext(conditions):%0A    value = conditions%5B'fulltext'%5D%0A    return 'must', %5Bdict(query_string=dict(query=value))%5D%0A%0A%0A@builder%0Adef from_(conditions):%0A    filters = dict()%0A    if 'from_' in conditions:%0A        filters%5B'gte'%5D = conditions%5B'from_'%5D.isoformat()%0A    if 'until' in conditions:%0A        filters%5B'lte'%5D = conditions%5B'until'%5D.isoformat()%0A    return 'must', %5Bdict(range=%7B%0A        'payload.document.last-semantic-change': filters%7D)%5D%0A%0A%0A@builder%0Adef until(conditions):%0A    if 'from_' not in conditions:%0A        return from_(conditions)%0A    return None, None%0A%0A%0A@builder%0Adef show_news(conditions):%0A    if not conditions%5B'show_news'%5D:%0A        return 'must_not', %5B%7B'payload.document.ressort': 'News'%7D%5D + %5B%0A            %7B'payload.workflow.product-id': pid%7D%0A            for pid in 'News', 'afp', 'SID', 'dpa-hamburg'%5D%0A    return None, None%0A%0A%0A
 fiel
@@ -809,18 +809,26 @@
 query(**
-kw
+conditions
 ):%0A    %22
@@ -1991,24 +1991,38 @@
 h%60.%0A    %22%22%22%0A
+    must = %5B%5D%0A
     clauses 
@@ -2038,174 +2038,942 @@
     
-for field, value in kw.items():%0A        if value in (None, %5B%5D, ()):%0A            continue%0A        elif field in builders:%0A            typ, clause = builders%5Bfield%5D(kw)
+# handle fulltext%0A    if 'fulltext' in conditions:%0A        value = conditions.pop('fulltext')%0A        must.append(dict(query_string=dict(query=value)))%0A    # handle from_, until%0A    if 'from_' in conditions or 'until' in conditions:%0A        filters = dict()%0A        if 'from_' in conditions:%0A            filters%5B'gte'%5D = conditions.pop('from_').isoformat()%0A        if 'until' in conditions:%0A            filters%5B'lte'%5D = conditions.pop('until').isoformat()%0A        must.append(dict(range=%7B%0A            'payload.document.last-semantic-change': filters%7D))%0A    # handle show_news%0A    if not conditions.pop('show_news', True):%0A        clauses%5B'must_not'%5D = %5B%7B'payload.document.ressort': 'News'%7D%5D + %5B%0A            %7B'payload.workflow.product-id': pid%7D%0A            for pid in 'News', 'afp', 'SID', 'dpa-hamburg'%5D%0A    # handle remaining fields%0A    for field, value in conditions.items():%0A        if value in (None, %5B%5D, ()):%0A            continue
 %0A   
@@ -3018,31 +3018,20 @@
     
-typ, clause = 'must', %5B
+must.append(
 dict
@@ -3063,17 +3063,17 @@
  value%7D)
-%5D
+)
 %0A       
@@ -3158,89 +3158,91 @@
     
-    if clause is not None:%0A            clauses.setdefault(typ, %5B%5D).extend(clause)
+# construct either bool or simple query%0A    if must:%0A        clauses%5B'must'%5D = must
 %0A   

Simplify code as there were too few 'builder' cases anyway.