aggregate a field in elasticsearch-dsl using python - python

Can someone tell me how to write Python statements that will aggregate (sum and count) stuff about my documents?
SCRIPT
from datetime import datetime
from elasticsearch_dsl import DocType, String, Date, Integer
from elasticsearch_dsl.connections import connections
from elasticsearch import Elasticsearch
from elasticsearch_dsl import Search, Q
# Define a default Elasticsearch client
client = connections.create_connection(hosts=['http://blahblahblah:9200'])
s = Search(using=client, index="attendance")
s = s.execute()
for tag in s.aggregations.per_tag.buckets:
print (tag.key)
OUTPUT
File "/Library/Python/2.7/site-packages/elasticsearch_dsl/utils.py", line 106, in __getattr__
'%r object has no attribute %r' % (self.__class__.__name__, attr_name))
AttributeError: 'Response' object has no attribute 'aggregations'
What is causing this? Is the "aggregations" keyword wrong? Is there some other package I need to import? If a document in the "attendance" index has a field called emailAddress, how would I count which documents have a value for that field?

First of all. I notice now that what I wrote here, actually has no aggregations defined. The documentation on how to use this is not very readable for me. Using what I wrote above, I'll expand. I'm changing the index name to make for a nicer example.
from datetime import datetime
from elasticsearch_dsl import DocType, String, Date, Integer
from elasticsearch_dsl.connections import connections
from elasticsearch import Elasticsearch
from elasticsearch_dsl import Search, Q
# Define a default Elasticsearch client
client = connections.create_connection(hosts=['http://blahblahblah:9200'])
s = Search(using=client, index="airbnb", doc_type="sleep_overs")
s = s.execute()
# invalid! You haven't defined an aggregation.
#for tag in s.aggregations.per_tag.buckets:
# print (tag.key)
# Lets make an aggregation
# 'by_house' is a name you choose, 'terms' is a keyword for the type of aggregator
# 'field' is also a keyword, and 'house_number' is a field in our ES index
s.aggs.bucket('by_house', 'terms', field='house_number', size=0)
Above we're creating 1 bucket per house number. Therefore, the name of the bucket will be the house number. ElasticSearch (ES) will always give a document count of documents fitting into that bucket. Size=0 means to give use all results, since ES has a default setting to return 10 results only (or whatever your dev set it up to do).
# This runs the query.
s = s.execute()
# let's see what's in our results
print s.aggregations.by_house.doc_count
print s.hits.total
print s.aggregations.by_house.buckets
for item in s.aggregations.by_house.buckets:
print item.doc_count
My mistake before was thinking an Elastic Search query had aggregations by default. You sort of define them yourself, then execute them. Then your response can be split b the aggregators you mentioned.
The CURL for the above should look like:
NOTE: I use SENSE an ElasticSearch plugin/extension/add-on for Google Chrome. In SENSE you can use // to comment things out.
POST /airbnb/sleep_overs/_search
{
// the size 0 here actually means to not return any hits, just the aggregation part of the result
"size": 0,
"aggs": {
"by_house": {
"terms": {
// the size 0 here means to return all results, not just the the default 10 results
"field": "house_number",
"size": 0
}
}
}
}
Work-around. Someone on the GIT of DSL told me to forget translating, and just use this method. It's simpler, and you can just write the tough stuff in CURL. That's why I call it a work-around.
# Define a default Elasticsearch client
client = connections.create_connection(hosts=['http://blahblahblah:9200'])
s = Search(using=client, index="airbnb", doc_type="sleep_overs")
# how simple we just past CURL code here
body = {
"size": 0,
"aggs": {
"by_house": {
"terms": {
"field": "house_number",
"size": 0
}
}
}
}
s = Search.from_dict(body)
s = s.index("airbnb")
s = s.doc_type("sleepovers")
body = s.to_dict()
t = s.execute()
for item in t.aggregations.by_house.buckets:
# item.key will the house number
print item.key, item.doc_count
Hope this helps. I now design everything in CURL, then use Python statement to peel away at the results to get what I want. This helps for aggregations with multiple levels (sub-aggregations).

I do not have the rep to comment yet but wanted to make a small fix on Matthew's comment on VISQL's answer regarding from_dict. If you want to maintain the search properties, use update_from_dict rather the from_dict.
According to the Docs , from_dict creates a new search object but update_from_dict will modify in place, which is what you want if Search already has properties such as index, using, etc
So you would want to declare the query body before the search and then create the search like this:
query_body = {
"size": 0,
"aggs": {
"by_house": {
"terms": {
"field": "house_number",
"size": 0
}
}
}
}
s = Search(using=client, index="airbnb", doc_type="sleep_overs").update_from_dict(query_body)

Related

Extracting certain value from MongoDB using Python

I have a mongo database including the following collection:
"
"_id": {
"$oid": "12345"
},
"id": "333555",
"token": [
{
"access_token": "ac_33bc",
"expires_in": 3737,
"token_type": "bearer",
"expires_at": {
"$date": "2021-07-02T13:37:28.123Z"
}
}
]
}
In the next python script I'm trying to return and print only the access_token but can't figure out how to do so. I've tried various methods which none of the worked.I've given the "id" as a parameter
def con_mongo():
try:
client = pymongo.MongoClient("mongodb:localhost")
#DB name
db = client["db1"]
#Collection
coll = db["coll1"]
#1st method
x = coll.find({"id":"333555"},{"token":"access_token"})
for data in x:
print(x)
#2nd method
x= coll.find({"id":"333555"})
tok=x.distinct("access_token")
#print(x[0])
for data in tok:
print(data)
except Exception:
logging.info(Exception)
It doesn't work this way, although if I replace (or remove) the "access_token" with simply "token" it works but I get back all the informations included in the field "token" where I only need the value of the "access_token".
Since access_token is an array element, you need to qualify it's name with the name of the array, to properly access its value.
Actually you can first extract the whole document and get the desired value through simple list and dict indexing.
So, assuming you are retrieving many documents with that same id:
x = [doc["token"][0]["access_token"] for doc in coll.find({"id":"333555"})]
The above, comprehensively creates a list with the access_tokens of all the documents matching the given id.
If you just need the first (and maybe only) occurrence of a document with that id, you can use find_one() instead:
x = coll.find_one({"id":"333555"})["token"][0]["access_token"]
# returns ac_33bc
token is a list so you have to reference the list element, e.g.
x = coll.find({"id":"333555"},{"token.access_token"})
for data in x:
print(data.get('token')[0].get('access_token'))
prints:
ac_33bc

How to pass a query result to a second query in pymongo?

My target was to run a query that involves text search. Below is my attempt:
from pymongo import MongoClient
from datetime import datetime
REMOTE_MONGO_URL = ""
mongo_connection = MongoClient(REMOTE_MONGO_URL)
xml_db = mongo_connection.some_string.some_other_string
#pprint(xml_db.index_information())
a = xml_db.find(
{
"source": "winterfell",
'expiration_date': {
'$gte': datetime.now().strftime('%Y-%m-%d')
},
"$text": {
'$search': "ned cat john arya sansa"
}
},
#hint='red_wedding'
)
print(a.count())
Initially, when I run the query, it just runs for infinite time, and I got the feeling that it's not using the proper index. So, I tried to impose the index with $hint. However, it fails with the message that I can't use $text and $hint together.
So, my plan is to first perform the initial query without text search (so that I can use $hint on it), and run the second query of text search on the result of first query. How can I do it?

Python GraphQL API call composition

I've recently started learning how to use python and i'm having some trouble with a graphQL api call.
I'm trying to set up a loop to grab all the information using pagination, and my first request is working just fine.
values = """
{"query" : "{organizations(ids:) {pipes {id name phases {id name cards_count cards(first:30){pageInfo{endCursor hasNextPage} edges {node {id title current_phase{name} assignees {name} due_date createdAt finished_at fields{name value filled_at updated_at} } } } } }}}"}
"""
but the second call using the end cursor as a variable isn't working for me. I assume that it's because i'm not understanding how to properly escape the string of the variable. But for the life of me I'm unable to understand how it should be done.
Here's what I've got for it so far...
values = """
{"query" : "{phase(id: """ + phaseID+ """ ){id name cards_count cards(first:30, after:"""\" + pointer + "\"""){pageInfo{endCursor hasNextPage} edges {node {id title assignees {name} due_date createdAt finished_at fields{name value datetime_value updated_at phase_field { id label } } } } } } }"}
"""
the second one as it loops just returns a 400 bad request.
Any help would be greatly appreciated.
As a general rule you should avoid building up queries using string manipulation like this.
In the GraphQL query itself, GraphQL allows variables that can be placeholders in the query for values you will plug in later. You need to declare the variables at the top of the query, and then can reference them anywhere inside the query. The query itself, without the JSON wrapper, would look something like
query = """
query MoreCards($phase: ID!, $cursor: String) {
phase(id: $phase) {
id, name, cards_count
cards(first: 30, after: $cursor) {
... CardConnectionData
}
}
}
"""
To actually supply the variable values, they get passed as an ordinary dictionary
variables = {
"phase": phaseID,
"cursor": pointer
}
The actual request body is a straightforward JSON structure. You can construct this as a dictionary too:
body = {
"query": query,
"variables": variables
}
Now you can use the standard json module to format it to a string
print(json.dumps(body))
or pass it along to something like the requests package that can directly accept the object and encode it for you.
I had a similar situation where I had to aggregate data through paginating from a GraphQL endpoint. Trying the above solution didn't work for me that well.
to start my header config for graphql was like this:
headers = {
"Authorization":f"Bearer {token}",
"Content-Type":"application/graphql"
}
for my query string, I used the triple quote with a variable placeholder:
user_query =
"""
{
user(
limit:100,
page:$page,
sort:[{field:"email",order:"ASC"}]
){
list{
email,
count
}
}
"""
Basically, I had my loop here for the pages:
for page in range(1, 9):
formatted_query = user_query.replace("$page",f'{page}')
response = requests.post(api_url, data=formatted_query,
headers=headers)
status_code, json = response.status_code, response.json()

How to mix JSON-serialized Django models with flat JSON

The problem I'm having is with mixing serialized Django models using django.core.serializers with some other piece of data and then trying to serialize the entire thing using json.dumps.
Example code:
scores = []
for indicator in indicators:
score_data = {}
score_data["indicator"] = serializers.serialize("json", [indicator,])
score_data["score"] = evaluation.indicator_percent_score(indicator.id)
score_data["score_descriptor"] = \
serializers.serialize("json",
[form.getDescriptorByPercent(score_data["score"]),],
fields=("order", "value", "title"))
scores.append(score_data)
scores = json.dumps(scores)
return HttpResponse(scores)
Which returns a list of objects like this:
{
indicator: "{"pk": 345345, "model": "forms.indicator", "fields": {"order": 1, "description": "Blah description.", "elements": [10933, 4535], "title": "Blah Title"}}",
score: 37.5,
score_descriptor: "{"pk": 66666, "model": "forms.descriptor", "fields": {"order": 1, "value": "1.00", "title": "Unsatisfactory"}}"
}
The problem I'm having can be seen in the JSON with the serialized Django models being wrapped in multiple sets of quotations. This makes the it very hard to work with on the client side as when I try to do something like
indicator.fields.order
it evaluates to nothing because the browser thinks I'm dealing with a string of some sort.
Ideally I would like valid JSON without the conflicting quotations that make it unreadable. Something akin to a list of objects like this:
{
indicator: {
pk: 12931231,
fields: {
order: 1,
title: "Blah Title",
}
},
etc.
}
Should I be doing this in a different order, using a different data structure, different serializer?
My solution involved dropping the use of django.core.serializers and instead using django.forms.model_to_dict and django.core.serializers.json.DjangoJSONEncoder.
The resulting code looked like this:
for indicator in indicators:
score_data = {}
score_data["indicator"] = model_to_dict(indicator)
score_data["score"] = evaluation.indicator_percent_score(indicator.id)
score_data["score_descriptor"] = \
model_to_dict(form.getDescriptorByPercent(score_data["score"]),
fields=("order", "value", "title"))
scores.append(score_data)
scores = json.dumps(scores, cls=DjangoJSONEncoder)
The problem seemed to arise from the fact that I was essentially serializing the Django models twice. Once with the django.core.serializers function and once with the json.dumps function.
The solution solved this by converting the model to a dictionary first, throwing it in the dictionary with the other data and then serializing it once using the json.dumps using the DjangoJSONEncoder.
Hope this helps someone out because I couldn't find my specific issue but was able to piece it together using other answer to other stackoverflow posts.

Modify JSON response of Flask-Restless

I am trying to use Flask-Restless with Ember.js which isn't going so great. It's the GET responses that are tripping me up. For instance, when I do a GET request on /api/people for example Ember.js expects:
{
people: [
{ id: 1, name: "Yehuda Katz" }
]
}
But Flask-Restless responds with:
{
"total_pages": 1,
"objects": [
{ "id": 1, "name": "Yahuda Katz" }
],
"num_results": 1,
"page": 1
}
How do I change Flask-Restless's response to conform to what Ember.js would like? I have this feeling it might be in a postprocessor function, but I'm not sure how to implement it.
Flask extensions have pretty readable source code. You can make a GET_MANY postprocessor:
def pagination_remover(results):
return {'people': results['objects']} if 'page' in results else results
manager.create_api(
...,
postprocessors={
'GET_MANY': [pagination_remover]
}
)
I haven't tested it, but it should work.
The accepted answer was correct at the time. However the post and preprocessors work in Flask-Restless have changed. According to the documentation:
The preprocessors and postprocessors for each type of request accept
different arguments, but none of them has a return value (more
specifically, any returned value is ignored). Preprocessors and
postprocessors modify their arguments in-place.
So now in my postprocessor I just delete any keys that I do not want. For example:
def api_post_get_many(result=None, **kw):
for key in result.keys():
if key != 'objects':
del result[key]

Categories