how to store a complex object in redis (using redis-py)
Asked Answered
Q

10

94

The hmset function can set the value of each field, but I found that if the value itself is a complex structured object, the value return from hget is a serialized string, not the original object

e.g

images= [{'type':'big', 'url':'....'},
     {'type':'big', 'url':'....'},
     {'type':'big', 'url':'....'}]   

redis = Redis()
redis.hset('photo:1', 'images', images)

i = redis.hget('photo:1', 'images')
print type(i)

the type of i is a string, not a python object, is there any way to solve this problem besides manually parse each fields?

Quoin answered 5/3, 2013 at 9:14 Comment(1)
Most answers try to solve the problem by serializing the complex object into a string, either by json or pickle. However, it's very inefficient when you try to modify the complex object. Instead, you can use redis-protobuf to save nested data structures to Redis. Check this answer for an example.Chesterfield
J
62

You can't create nested structures in Redis, meaning you can't (for example) store a native redis list inside a native redis hash-map.

If you really need nested structures, you might want to just store a JSON-blob (or something similar) instead. Another option is to store an "id"/key to a different redis object as the value of the map key, but that requires multiple calls to the server to get the full object.

Jural answered 5/3, 2013 at 9:20 Comment(1)
Oh, and one more thing; It might be possible to make a weird compound-query using EVAL (server-side ruby scripting): redis.io/commands/evalJural
L
155

Actually, you can store python objects in redis using the built-in module pickle.

Here is example.

import pickle
import redis

r = redis.StrictRedis(host='localhost', port=6379, db=0)
obj = ExampleObject()
pickled_object = pickle.dumps(obj)
r.set('some_key', pickled_object)
unpacked_object = pickle.loads(r.get('some_key'))
obj == unpacked_object
Levator answered 5/12, 2013 at 12:39 Comment(13)
This is dangerous: unpickling can execute code. The JSON solution is more robust.Mucoid
@EOL can you elborate further on this? why would executing code a bad thing?Frazzle
I meant that pickling allows the execution of untrusted code, which is better avoided, since the code in question can be malicious (it can erase files, etc.).Mucoid
@EOL i don't think that your redis is untrusted source. You can not stop people from shooting themselves in the footLevator
@KyryloPerevozchikov: OK, fair enough, since the original question indeed connects locally. No downvote from me. The local Redis might still be a local copy of an untrusted source, so I'll leave my comment above, it cannot hurt. :)Mucoid
It's better to use cPickle than pickle, because cPickle has better performance.Rudderpost
@EOL: It seems to me JSON is preferred when possible, but there are a great number of objects that cannot be serialized into JSON.Cockburn
@CivFan: Good point: this would mostly be my criterion for using pickle instead of JSON, in the case of the original question (the other criterion would be whether the source is trustable). That said, it is often possible to make objects JSON-serializable, but this typically involves writing custom JSON writing/reading code, which is not nearly as convenient as using pickle, indeed.Mucoid
As an examples struct_time isn't json serializable, but it's serializable by pickle.Superordinate
If you debug into redis library, I believe it pickles your data before it puts it in redis. At least using flask_caching which I believe calls the underlying redis caching code. I was actually looking to see if there were an option to change this behavior or to use a safer pickle like serpent.Cristiano
csv? first row can be the header and rest can be super compressed compared to jsonNeoimpressionism
I just tested with a few objects. Pickle is slower that Json blob. For both dump and load.Suzysuzzy
Pickle format can be upgrade resistant - when maintaining our ML models I've tried to upgrade various pickled objects from python versions ranging from 3.6 to 3.9 and unpickling can fail even between two neighboring minor versions...Howes
C
72

If your data is JSON-serializable, then that may be the better option than saving python pickles to an external database, since it's a more common standard outside of Python, is more human-readable on its own, and avoids a rather large attack vector.

JSON Example:

import json
import redis

r = redis.StrictRedis(host='localhost', port=6379, db=0)

images= [
    {'type':'big', 'url':'....'},
    {'type':'big', 'url':'....'},
    {'type':'big', 'url':'....'},
]

# Convert python dict to JSON str and save to Redis
json_images = json.dumps(images)
r.set('images', json_images)

# Read saved JSON str from Redis and unpack into python dict
unpacked_images = json.loads(r.get('images'))
images == unpacked_images

python 3:

unpacked_images = json.loads(r.get('images').decode('utf-8'))
images == unpacked_images
Cockburn answered 9/9, 2015 at 18:1 Comment(3)
is it normal to get btyes object from redis.get()? i got b'['something']' from it in python 3Intermolecular
@shangyeshen Looks like json.loads() doesn't play nice with bytes objects in python3. You'll need to decode the bytes result from redis.get() into a python3 str. See the new edit.Cockburn
You can also pass decode_responses=True to StrictRedis (or just Redis in version > 3.0 #19022265).Coruscate
J
62

You can't create nested structures in Redis, meaning you can't (for example) store a native redis list inside a native redis hash-map.

If you really need nested structures, you might want to just store a JSON-blob (or something similar) instead. Another option is to store an "id"/key to a different redis object as the value of the map key, but that requires multiple calls to the server to get the full object.

Jural answered 5/3, 2013 at 9:20 Comment(1)
Oh, and one more thing; It might be possible to make a weird compound-query using EVAL (server-side ruby scripting): redis.io/commands/evalJural
I
9

I created a library, SubRedis, which lets you create much more complex structures/hierarchies in redis. If you give it a redis instance and a prefix, it gives you a nearly fully capable and independent redis instance.

redis = Redis()
photoRedis = SubRedis("photo:%s" % photoId, redis)
photoRedis.hmset('image0', images[0])
photoRedis.hmset('image1', images[1])
...

SubRedis just ends up prepending the string passed into it as a prefix onto the flat redis data structure. I find this to be a convenient wrapper for a pattern I end up doing a lot in redis -- prepending some id to nest some data.

Incoordinate answered 1/2, 2014 at 2:58 Comment(0)
H
8

Here is a simple wrapper around Redis which pickles/unpickles data structures:

from redis import Redis
from collections import MutableMapping
from pickle import loads, dumps


class RedisStore(MutableMapping):

    def __init__(self, engine):
        self._store = Redis.from_url(engine)

    def __getitem__(self, key):
        return loads(self._store[dumps(key)])

    def __setitem__(self, key, value):
        self._store[dumps(key)] = dumps(value)

    def __delitem__(self, key):
        del self._store[dumps(key)]

    def __iter__(self):
        return iter(self.keys())

    def __len__(self):
        return len(self._store.keys())

    def keys(self):
        return [loads(x) for x in self._store.keys()]

    def clear(self):
        self._store.flushdb()


d = RedisStore('redis://localhost:6379/0')
d['a'] = {'b': 1, 'c': 10}
print repr(d.items())
# this will not work: (it updates a temporary copy and not the real data)
d['a']['b'] = 2
print repr(d.items())
# this is how to update sub-structures:
t = d['a']
t['b'] = 2
d['a'] = t
print repr(d.items())
del d['a']

# Here is another way to implement dict-of-dict eg d['a']['b']
d[('a', 'b')] = 1
d[('a', 'b')] = 2
print repr(d.items())
# Hopefully you do not need the equivalent of d['a']
print repr([{x[0][1]: x[1]} for x in d.items() if x[0][0] == 'a'])
del d[('a', 'b')]
del d[('a', 'c')]

If you prefer plaintext-readable data in redis (pickle stores a binary version of it), you can replace pickle.dumps with repr and pickle.loads with ast.literal_eval. For json, use json.dumps and json.loads.

If you always use keys which are a simple string, you can remove the pickling from the key.

Heeley answered 19/2, 2018 at 17:14 Comment(0)
U
7

You can use RedisJSON from RedisLabs with client for python. It's supported the nested data structure. Very useful for tasks like this.

Some code from the example:

   # Set the key `obj` to some object
   obj = {
       'answer': 42,
       'arr': [None, True, 3.14],
       'truth': {
           'coord': 'out there'
       }
   }
   rj.jsonset('obj', Path.rootPath(), obj)

   # Get something
   print 'Is there anybody... {}?'.format(
       rj.jsonget('obj', Path('.truth.coord'))
   )
Unfortunate answered 1/5, 2020 at 10:15 Comment(1)
Also, if using Docker to install Redis, do not forget to use the ReJSON module. This is the Docker image: hub.docker.com/r/redislabs/rejson. Since hub.docker.com/_/redis there's not the ReJSON module installed by default.Unhappy
O
6

You can use RedisWorks library.

pip install redisworks

>>> from redisworks import Root
>>> root = Root()
>>> root.something = {1:"a", "b": {2: 2}}  # saves it as Hash
>>> print(root.something)  # loads it from Redis
{'b': {2: 2}, 1: 'a'}
>>> root.something['b'][2]
2

It converts python types to Redis types and vice-versa.

>>> root.sides = [10, [1, 2]]  # saves it as list in Redis.
>>> print(root.sides)  # loads it from Redis
[10, [1, 2]]
>>> type(root.sides[1])
<class 'list'>

Disclaimer: I wrote the library. Here is the code: https://github.com/seperman/redisworks

Ouabain answered 30/8, 2016 at 5:47 Comment(0)
A
1
 for saving object in redis first convert object into stringify JSON
 StringifyImages = json.dumps(images)
redis.set('images', StringifyImages)

# Read stringify object from redis and parse it 
ParseImages = json.loads(redis.get('images'))
Af answered 20/10, 2021 at 7:55 Comment(0)
D
0

I have faced a similar use case recently. Storing a complex data structure in redis hash.

I think the best way to resolve the issue is by serialiasing the json object to string and store it as value for another object.

Typescript example

Object to store in hashmap

const payload = {
 "k1":"v1",
 "k2": "v2",
 "k3": {
     "k4":"v4",
     "k5":"v5"
  }
}

Store this payload as

await redis.hmset('hashMapKey', {somePayloadKey: JSON.stringify(payload) });

This can be retrieved as

      const result = await redis.hgetall('hashMapKey');
      const payload = JSON.parse(result.somePayloadKey);

hmset and hgetall are tedis equivalents to HMSET and HGETALL in redis.

Hope this helps.

Definitive answered 8/7, 2020 at 6:32 Comment(0)
S
-3

You can just store your structure as is and do an 'eval' to convert from String to Object:

images= [{'type':'big', 'url':'....'},
 {'type':'big', 'url':'....'},
 {'type':'big', 'url':'....'}]   
redis = Redis()
redis.hset('photo:1', 'images', images)

i = eval(redis.hget('photo:1', 'images'))
print type(i) #type of i should be list instead of string now
Shenika answered 21/3, 2013 at 21:43 Comment(1)
This is unnecessarily dangerous: arbitrary Python code is evaluated through eval(). The JSON approach does not suffer from this.Mucoid

© 2022 - 2024 — McMap. All rights reserved.