Repository: elastic/elasticsearch-dsl-py

Issue:
TypeError unhashable type: 'list'
```
    client = Search(using=haystack_connections['default'].get_backend().conn, index='haystack', doc_type='modelresult')
    query = \
        {
            "size": 0,

            "query" : {
                "constant_score": {
                    "filter": [
                        {
                          "missing": {
                            "field": "deleted_at"
                          }
                        }
                    ]

                }
            },

            "aggs" : {
                "occurrences_of_query" : {
                    "filter" : {
                      "match": {
                        "text": "query"
                      }
                    },

                    "aggs": {

                       "city_slug": {
                           "terms": {
                               "field": "geo_type_slug",
                               "size": 6,
                               "order": {
                                "city_distance" : "asc"
                              }
                           },
                           "aggs": {
                               "city_distance": {
                                   "avg": {
                                       "script": {
                                           "inline": "doc['coordinates'].arcDistanceInKm(ref_lat, ref_lon)",
                                           "params": {
                                                "ref_lat": 61.25595,
                                                "ref_lon": 73.384547
                                           }
                                       }
                                   }
                               }
                            }
                       }
                    }
                }
            }
        }


    searcher = client.update_from_dict(query)
    response = searcher.execute()
```
Which rises:

```
File "/Users/alkalit/.virtualenvs/promvtor/promvtor/apps/seo_queries/jinja/globals.py" in get_seo_queries
  174.     searcher = client.update_from_dict(query)
File "/Users/alkalit/.virtualenvs/promvtor/promvtor/apps/elasticsearch_dsl2/search.py" in update_from_dict
  329.             self.query._proxied = Q(d.pop('query'))
File "/Users/alkalit/.virtualenvs/promvtor/promvtor/apps/elasticsearch_dsl2/query.py" in Q
  27.         return Query.get_dsl_class(name)(**params)
File "/Users/alkalit/.virtualenvs/promvtor/promvtor/apps/elasticsearch_dsl2/utils.py" in __init__
  221.             self._setattr(pname, pvalue)
File "/Users/alkalit/.virtualenvs/promvtor/promvtor/apps/elasticsearch_dsl2/utils.py" in _setattr
  268.                     value = shortcut(value)
File "/Users/alkalit/.virtualenvs/promvtor/promvtor/apps/elasticsearch_dsl2/query.py" in Q
  40.     return Query.get_dsl_class(name_or_query)(**params)
File "/Users/alkalit/.virtualenvs/promvtor/promvtor/apps/elasticsearch_dsl2/utils.py" in get_dsl_class
  212.             return cls._classes[name]
```

Where is:
```
name 	

[{u'missing': {u'field': u'deleted_at'}}]

cls 	

<class 'elasticsearch_dsl2.query.Query'>
```

```
In [1]: import elasticsearch_dsl2

In [2]: elasticsearch_dsl2.VERSION
Out[2]: (2, 2, 0)

In [3]: import elasticsearch

In [4]: elasticsearch.VERSION
Out[4]: (2, 1, 0)
```



Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index f6456d7..88ce371 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,6 +16,10 @@ python:
   - "3.3"
   - "3.4"
   - "3.5"
+  - "3.6"
+  - "3.6-dev"
+  - "3.7-dev"
+  - "nightly"
 
 env:
   global:
diff --git a/docs/index.rst b/docs/index.rst
index d19c3ae..6db9555 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -23,6 +23,9 @@ Compatibility
 The library is compatible with all Elasticsearch versions since ``1.x`` but you
 **have to use a matching major version**:
 
+For **Elasticsearch 5.0** and later, use the major version 5 (``5.x.y``) of the
+library.
+
 For **Elasticsearch 2.0** and later, use the major version 2 (``2.x.y``) of the
 library.
 
@@ -33,6 +36,9 @@ library.
 The recommended way to set your requirements in your `setup.py` or
 `requirements.txt` is::
 
+    # Elasticsearch 5.x
+    elasticsearch-dsl>=5.0.0,<6.0.0
+
     # Elasticsearch 2.x
     elasticsearch-dsl>=2.0.0,<3.0.0
 
diff --git a/docs/search_dsl.rst b/docs/search_dsl.rst
index 7b8fe32..6e730cc 100644
--- a/docs/search_dsl.rst
+++ b/docs/search_dsl.rst
@@ -417,7 +417,9 @@ endpoint) you can do so via ``execute_suggest``:
 Extra properties and parameters
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-To set extra properties of the search request, use the ``.extra()`` method:
+To set extra properties of the search request, use the ``.extra()`` method.
+This can be used to define keys in the body that cannot be defined via a
+specific API method like ``explain`` or ``search_after``:
 
 .. code:: python
 
diff --git a/elasticsearch_dsl/aggs.py b/elasticsearch_dsl/aggs.py
index 9d9058a..84b5b8b 100644
--- a/elasticsearch_dsl/aggs.py
+++ b/elasticsearch_dsl/aggs.py
@@ -40,6 +40,10 @@ class Agg(DslBase):
     _type_name = 'agg'
     _type_shortcut = staticmethod(A)
     name = None
+
+    def __contains__(self, key):
+        return False
+
     def to_dict(self):
         d = super(Agg, self).to_dict()
         if 'meta' in d[self.name]:
@@ -54,6 +58,9 @@ class AggBase(object):
     _param_defs = {
         'aggs': {'type': 'agg', 'hash': True},
     }
+    def __contains__(self, key):
+        return key in self._params.get('aggs', {})
+
     def __getitem__(self, agg_name):
         agg = self._params.setdefault('aggs', {})[agg_name] # propagate KeyError
 
diff --git a/elasticsearch_dsl/field.py b/elasticsearch_dsl/field.py
index a1afb18..3fbc80d 100644
--- a/elasticsearch_dsl/field.py
+++ b/elasticsearch_dsl/field.py
@@ -46,6 +46,9 @@ class Field(DslBase):
         self._required = kwargs.pop('required', False)
         super(Field, self).__init__(*args, **kwargs)
 
+    def __getitem__(self, subfield):
+        return self._params.get('fields', {})[subfield]
+
     def _serialize(self, data):
         return data
 
@@ -265,6 +268,9 @@ class Boolean(Field):
 class Float(Field):
     name = 'float'
 
+class HalfFloat(Field):
+    name = 'half_float'
+
 class Double(Field):
     name = 'double'
 
diff --git a/elasticsearch_dsl/query.py b/elasticsearch_dsl/query.py
index 6bcd69a..b5a1225 100644
--- a/elasticsearch_dsl/query.py
+++ b/elasticsearch_dsl/query.py
@@ -179,7 +179,10 @@ class Boosting(Query):
 
 class ConstantScore(Query):
     name = 'constant_score'
-    _param_defs = {'query': {'type': 'query'}, 'filter': {'type': 'query'}}
+    _param_defs = {
+        'query': {'type': 'query', 'multi': True},
+        'filter': {'type': 'query', 'multi': True}
+    }
 
 class DisMax(Query):
     name = 'dis_max'
diff --git a/elasticsearch_dsl/response/__init__.py b/elasticsearch_dsl/response/__init__.py
index 4e8b1de..f6bd47e 100644
--- a/elasticsearch_dsl/response/__init__.py
+++ b/elasticsearch_dsl/response/__init__.py
@@ -30,11 +30,18 @@ class Response(AttrDict):
     def __len__(self):
         return len(self.hits)
 
+    def __getstate__(self):
+        return (self._d_, self._search)
+
+    def __setstate__(self, state):
+        super(AttrDict, self).__setattr__('_d_', state[0])
+        super(AttrDict, self).__setattr__('_search', state[1])
+
     def success(self):
         return self._shards.total == self._shards.successful and not self.timed_out
 
     def _get_result(self, hit):
-        dt = hit['_type']
+        dt = hit.get('_type')
         for t in hit.get('inner_hits', ()):
             hit['inner_hits'][t] = Response(self._search, hit['inner_hits'][t])
         callback = self._search._doc_type_map.get(dt, Hit)
diff --git a/elasticsearch_dsl/search.py b/elasticsearch_dsl/search.py
index fc71204..b49d982 100644
--- a/elasticsearch_dsl/search.py
+++ b/elasticsearch_dsl/search.py
@@ -43,6 +43,12 @@ class QueryProxy(object):
             setattr(self._proxied, attr_name, value)
         super(QueryProxy, self).__setattr__(attr_name, value)
 
+    def __getstate__(self):
+        return (self._search, self._proxied, self._attr_name)
+
+    def __setstate__(self, state):
+        self._search, self._proxied, self._attr_name = state
+
 
 class ProxyDescriptor(object):
     """
@@ -96,6 +102,15 @@ class Request(object):
         self._params = {}
         self._extra = extra or {}
 
+    def __eq__(self, other):
+        return (
+            isinstance(other, Request) and
+            other._params == self._params and
+            other._index == self._index and
+            other._doc_type == self._doc_type and
+            other.to_dict() == self.to_dict()
+        )
+
     def params(self, **kwargs):
         """
         Specify query params to be used when executing the search. All the


Test patch:
diff --git a/test_elasticsearch_dsl/conftest.py b/test_elasticsearch_dsl/conftest.py
index a07bf09..b2c2dd8 100644
--- a/test_elasticsearch_dsl/conftest.py
+++ b/test_elasticsearch_dsl/conftest.py
@@ -132,6 +132,7 @@ def aggs_search():
         .metric('line_stats', 'stats', field='stats.lines')\
         .metric('top_commits', 'top_hits', size=2, _source=["stats.*", "committed_date"])
     s.aggs.bucket('per_month', 'date_histogram', interval='month', field='info.committed_date')
+    s.aggs.metric('sum_lines', 'sum', field='stats.lines')
     return s
 
 @fixture
@@ -142,6 +143,7 @@ def aggs_data():
         '_shards': {'total': 1, 'successful': 1, 'failed': 0},
         'hits': {'total': 52, 'hits': [], 'max_score': 0.0},
         'aggregations': {
+            'sum_lines': {'value': 25052.0},
             'per_month': {
                 'buckets': [
                     {'doc_count': 38, 'key': 1393632000000, 'key_as_string': '2014-03-01T00:00:00.000Z'},
diff --git a/test_elasticsearch_dsl/test_mapping.py b/test_elasticsearch_dsl/test_mapping.py
index 350c63b..f42d866 100644
--- a/test_elasticsearch_dsl/test_mapping.py
+++ b/test_elasticsearch_dsl/test_mapping.py
@@ -152,3 +152,9 @@ def test_even_non_custom_analyzers_can_have_params():
             }
             }
     } == m._collect_analysis()
+
+def test_resolve_field_can_resolve_multifields():
+    m = mapping.Mapping('m')
+    m.field('title', 'text', fields={'keyword': Keyword()})
+
+    assert isinstance(m.resolve_field('title.keyword'), Keyword)
diff --git a/test_elasticsearch_dsl/test_query.py b/test_elasticsearch_dsl/test_query.py
index c1414e1..7cc0fe5 100644
--- a/test_elasticsearch_dsl/test_query.py
+++ b/test_elasticsearch_dsl/test_query.py
@@ -212,6 +212,17 @@ def test_two_bool_queries_append_one_to_should_if_possible():
     assert (q1 | q2) == query.Bool(should=[query.Match(f='v'), query.Bool(must=[query.Match(f='v')])])
     assert (q2 | q1) == query.Bool(should=[query.Match(f='v'), query.Bool(must=[query.Match(f='v')])])
 
+def test_allow_multiple_queries_for_constant_score_issue_599():
+    # Construct a number of ConstantScore queries each with different
+    # combinations of queries and filters.
+    q1 = query.ConstantScore(query=[query.Match(f=1), query.Match(f=2)])
+    q2 = query.ConstantScore(filter=[query.Match(f=1), query.Match(f=2)])
+    q3 = query.ConstantScore(query=query.Match(f=1))
+    q4 = query.ConstantScore(filter=query.Match(f=1))
+
+    for q in [q1, q2, q3, q4]:
+        assert isinstance(q.to_dict(), dict)
+
 def test_queries_are_registered():
     assert 'match' in query.Query._classes
     assert query.Query._classes['match'] is query.Match
diff --git a/test_elasticsearch_dsl/test_result.py b/test_elasticsearch_dsl/test_result.py
index af4265a..e7b36ed 100644
--- a/test_elasticsearch_dsl/test_result.py
+++ b/test_elasticsearch_dsl/test_result.py
@@ -15,6 +15,8 @@ def test_agg_response_is_pickleable(agg_response):
     r = pickle.loads(pickle.dumps(agg_response))
 
     assert r == agg_response
+    assert r._search == agg_response._search
+    assert r.hits == agg_response.hits
 
 def test_response_is_pickleable(dummy_response):
     res = response.Response(Search(), dummy_response)
@@ -22,6 +24,8 @@ def test_response_is_pickleable(dummy_response):
     r = pickle.loads(pickle.dumps(res))
 
     assert r == res
+    assert r._search == res._search
+    assert r.hits == res.hits
 
 def test_hit_is_pickleable(dummy_response):
     res = response.Response(Search(), dummy_response)
@@ -120,10 +124,13 @@ def test_aggregation_base(agg_response):
     assert agg_response.aggs is agg_response.aggregations
     assert isinstance(agg_response.aggs, response.AggResponse)
 
+def test_metric_agg_works(agg_response):
+    assert 25052.0 == agg_response.aggs.sum_lines.value
+
 def test_aggregations_can_be_iterated_over(agg_response):
     aggs = [a for a in agg_response.aggs]
 
-    assert len(aggs) == 2
+    assert len(aggs) == 3
     assert all(map(lambda a: isinstance(a, AggResponse), aggs))
 
 def test_aggregations_can_be_retrieved_by_name(agg_response, aggs_search):