How to bulk update results after scan query in Elasticsearch python? - python

I am performing adding tags to my elasticsearch document from python. The current way I am doing is by making scan function call and getting all the documents back and then updating each document with the tag I want to append. The code is following.
query_body = {
"query": {
"match_phrase": {
"message" : "some_keyword"
}
}
}
client_info = Elasticsearch()
index = "some_index"
# getting all the documents that contains some_keyword
result = helpers.scan(client_info, index=tag_info['index'], query=query_body)
Now, after getting all the documents that contain the some_keyword. I run a loop that will go through each document and update the tags field.
The following code gets the already existing tags in old_tags variable if they exits and then append the [tags_to_add] to old_tags otherwise it creates a new list of tags. Next it makes an update call to the elasticsearch instance.
for record in result:
old_tags = (record['_source']['tags'])
if(old_tags):
for tag in tag_info['tags_to_add']:
if tag not in old_tags:
old_tags.append(tag)
bd = {
"doc": {
"tags" : old_tags
}
}
#updates the particular document with new tags
res = client.update(index=tag_info['index'], id=record['_id'], body=bd)
else:
old_tags = []
for tag in tag_info['tags_to_add']:
old_tags.append(tag)
bd = {
"doc": {
"tags" : old_tags
}
}
res = client.update(index=tag_info['index'], id=record['_id'], body=bd)
Now, the problem is it is looping through each document at a time and then makes the update call to elasticsearch, which is okay for few documents but an expensive operation if done for a big document, half a million in my case.
Now, I want to learn if there is a bulk operation I can perform to update tags in bulk to save time.
Any information would be helpful.
Thank you in advance!

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?

How to store what key of a JSON to be parsed by a script in another JSON?

Suppose I have a JSON called jsondata.json:
{
"apiURL": [
{
"name":"Target",
"url":"https://redsky.target.com/v2/plp/collection/13562231,14919690,13728423,14919033,13533833,13459265,14917313,13519319,13533837,14919691,13479115,47778362,15028201,51467685,50846848,50759802,50879657,13219631,13561421,52062271,14917361,51803965,13474244,13519318?key=eb2551e4accc14f38cc42d32fbc2b2ea&pricing_store_id=2088&multichannel_option=basics&storeId=321"
},
{
"name":"Safeway",
"url":"https://shop.safeway.com/bin/safeway/product/aemaisle?aisleId=1_23_2&storeId=1483"
}
]
}
I want to tell my script to retrieve data from the API the url contains as follows:
# Load JSON containing URLs of APIs of grocery stores
with open(json_data, 'r') as data_f:
data_dict = json.load(data_f)
# Organize API URLs
for apiurl in data_dict['apiURL']:
responses.append('')
responses[index] = requests.get(apiurl['url'])
responses[index].raise_for_status()
storenames.append(apiurl['name'])
index += 1
first_target_item = responses[0].json()['search_response']['items']['Item'][0]['title']
first_safeway_item = responses[1].json()['productsinfo'][0]['description']
As you can see, my current implementation requires me to manually enter to my script which key to parse from each API (last two lines). I want to eventually be able to retrieve information from a dynamic number of grocery stores, but each website stores data on their items in a different key of their API.
How can I automate the process (e.g. store the key to parse from in jsondata.json) so that I don't have to update my script every time I add a new grocery store?
If you are okay with modifying the jsondata.json you can keep an array like this:
{
"name":"Target",
"accessKeys": ["search_response", "items", "Item", "0", "title"],
"url":"https://redsky.target.com/v2/plp/collection/13562231,14919690,13728423,14919033,13533833,13459265,14917313,13519319,13533837,14919691,13479115,47778362,15028201,51467685,50846848,50759802,50879657,13219631,13561421,52062271,14917361,51803965,13474244,13519318?key=eb2551e4accc14f38cc42d32fbc2b2ea&pricing_store_id=2088&multichannel_option=basics&storeId=321",
}
In your python code:
keys=["search_response", "items", "Item", "0", "title"] #apiUrl['accessKeys']
target_item=responses[0].json()
for i in target_item:
target_item=target_item[i]
You can automate more,
def get_keys(data, keys):
for key in keys:
data=data[key]
return data
items=[]
for index, apiurl in enumerate(data_dict['apiURL']):
responses.append('')
responses[index] = requests.get(apiurl['url'])
responses[index].raise_for_status()
storenames.append(apiurl['name'])
items.append(get_keys(responses[index].json(), apiUrl['accessKeys']))

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()

aggregate a field in elasticsearch-dsl using 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)

Categories