Как пересечь набор Python с набором Redis

У меня есть набор в коде python, скажем, item_ids

item_ids = {1, 2, 3, 4, 5}

А также у меня есть заранее рассчитанный набор redis, items_with_property

127.0.0.1:6379> SADD items_with_property 3 4 5 6
(integer) 5
127.0.0.1:6379> SMEMBERS items_with_property
1) "3"
2) "4"
3) "5"
4) "6"

Возможно ли пересечь item_ids с items_with_property без извлечения items_with_property в память Python?

я хочу что-то вроде

>>> print(redis_client.magic_sinter(item_ids, 'items_with_property'))
{3, 4, 5}

Причина этого в том, что items_with_property и item_ids могут быть очень большими, и я не хочу передавать много данных между серверами (redis находится на отдельной машине и имеет много клиентов)

ОБНОВЛЕНИЕ 2019-02-05

Я подготовил тест скорости для разных подходов, вот мои результаты:

from django.core.cache import caches
from django.utils.functional import cached_property
from django_redis import get_redis_connection
from hot_redis import Set

search_storage = caches['search.filter']


class Command(BaseCommand):

    command_name = 'performance_test'

    def handle(self, *args, **options):
        """
        output:
        1
            TEST1 results time:  13.432172060012817
            TEST2 results time:  4.478500127792358
            TEST3 results time:  4.45565390586853
            TEST4 results time:  4.674767732620239
            TEST5 results time:  3.244804859161377
            TEST6 results time:  4.4963860511779785

        2
            TEST1 results time:  13.012064695358276
            TEST2 results time:  4.4086668491363525
            TEST3 results time:  4.4962310791015625
            TEST4 results time:  4.745664119720459
            TEST5 results time:  3.3029701709747314
            TEST6 results time:  4.676959991455078

        3
            TEST1 results time:  12.83815312385559
            TEST2 results time:  4.190127849578857
            TEST3 results time:  4.445873260498047
            TEST4 results time:  4.724813938140869
            TEST5 results time:  3.2511937618255615
            TEST6 results time:  4.454891920089722
        4
            TEST1 results time:  13.131163358688354
            TEST2 results time:  4.265545129776001
            TEST3 results time:  4.440964221954346
            TEST4 results time:  4.571079969406128
            TEST5 results time:  3.279599189758301
            TEST6 results time:  4.366865873336792
        5
            TEST1 results time:  13.424093961715698
            TEST2 results time:  4.349413156509399
            TEST3 results time:  4.42648720741272
            TEST4 results time:  4.607520818710327
            TEST5 results time:  3.415123224258423
            TEST6 results time:  4.391672134399414
        """
        item_ids = set(random.sample(range(10000000, 1000000000), 100000))  # 100k random ints

        # TEST1 - PYTHON INTERSECTION
        _started_at = time.time()
        _key = f'test1'
        search_storage.set(_key, item_ids)  # python pickled set
        for _ in range(1000):
            search_ids = set(random.sample(item_ids, k=100))
            redis_ids = search_storage.get(_key)
            result = search_ids & redis_ids
            assert len(result) == 100
        search_storage.delete(_key)
        print("TEST1 results time: ", time.time() - _started_at)

        # TEST2 - REDIS INTERSECTION, using stored function and SISMEMBER for every search_id
        _started_at = time.time()
        _key = f'test2'
        redis_con = get_redis_connection('search.filter')  # raw connetction for redis methods.
        redis_con.sadd(_key, *item_ids)
        stored_func = redis_con.register_script('''
        local reply = {}
        while #ARGV > 0 do
          local member = table.remove(ARGV)
          if redis.call('SISMEMBER', KEYS[1], member) == 1 then
            table.insert(reply, member)
          end
        end
        return reply
        ''')
        for _ in range(1000):
            search_ids = random.sample(item_ids, k=100)
            result = stored_func(keys=[_key], args=search_ids)
            assert len(result) == 100
        redis_con.delete(_key)
        print("TEST2 results time: ", time.time() - _started_at)

        # TEST3 - REDIS INTERSECTION, using python-made temp key
        _started_at = time.time()
        _key = f'test3'
        redis_con = get_redis_connection('search.filter')
        redis_con.sadd(_key, *item_ids)
        for _ in range(1000):
            search_ids = frozenset(random.sample(item_ids, k=100))
            _temp_key = f'test3_temp_{hash(search_ids)}'
            redis_con.sadd(_temp_key, *search_ids)
            result = redis_con.sinter(keys=[_key, _temp_key])
            redis_con.delete(_temp_key)
            assert len(result) == 100
        redis_con.delete(_key)
        print("TEST3 results time: ", time.time() - _started_at)

        # TEST4 - REDIS INTERSECTION, using stored function and redis-made temp key
        _started_at = time.time()
        _key = f'test4'
        redis_con = get_redis_connection('search.filter')
        redis_con.sadd(_key, *item_ids)
        stored_func = redis_con.register_script('''
        local reply = {}
        local temp_key = KEYS[1]
        redis.call('SADD', temp_key, unpack(ARGV))
        reply = redis.call('SINTER', temp_key, KEYS[2])
        redis.call('DEL', temp_key)
        return reply
        ''')
        for _ in range(1000):
            search_ids = frozenset(random.sample(item_ids, k=100))
            _temp_key = f'test4_temp_{hash(search_ids)}'
            result = stored_func(keys=[_temp_key, _key], args=search_ids)
            assert len(result) == 100
        redis_con.delete(_key)
        print("TEST4 results time: ", time.time() - _started_at)

        # TEST5 - PYTHON INTERSECTION, using cached_property
        _started_at = time.time()
        _key = f'test5'
        search_storage.set(_key, item_ids, SEARCH_FILTER_TIMEOUT)
        for _ in range(1000):
            search_ids = set(random.sample(item_ids, k=100))
            redis_ids = self.cached_cached_item_ids
            result = search_ids & redis_ids
            assert len(result) == 100
        search_storage.delete(_key)
        print("TEST5 results time: ", time.time() - _started_at)

        # TEST6 - HOT REDIS
        _started_at = time.time()
        _key = f'test6'
        hot_items = Set(key=_key, initial=item_ids)
        for _ in range(1000):
            search_ids = Set(initial=random.sample(item_ids, k=100))
            result = hot_items & search_ids
            search_ids.clear()
            assert len(result) == 100
        hot_items.clear()
        print("TEST6 results time: ", time.time() - _started_at)

    @cached_property
    def cached_cached_item_ids(self):
        return search_storage.get('test5')

Вы можете попробовать самостоятельно, это команда Django, но я думаю, что суть ясна.

Победителем стал TEST5 — кэшированные результаты с «локальным кэшированием». Мы значительно сокращаем время сериализации данных, но у нас есть другая головная боль — как аннулировать второй уровень кэширования.

Для меня победителем стала горячий редис — библиотека python, в которой уже реализованы все соответствующие луа-методы и которая имеет чистый интерфейс.

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
0
729
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Есть такой волшебный способ сделать это на любом языке с помощью Lua (еще один язык). См. Redis EVAL и вспомогательный класс Script redis-py.

Сценарий ожидает имя ключа набора Redis и любое количество дополнительных аргументов, представляющих набор на стороне клиента («item_ids»). Для каждого аргумента он выполняет операцию SISMEMBER над целевым набором, чтобы определить пересечение.

from redis import Redis

items_with_property = [3, 4, 5, 6]
item_ids = [1, 2, 3, 4, 5]

r = Redis()
r.sadd('items_with_property', *items_with_property)
s = r.register_script('''
local reply = {}
while #ARGV > 0 do
  local member = table.remove(ARGV)
  if redis.call('SISMEMBER', KEYS[1], member) == 1 then
    table.insert(reply, member)
  end
end
return reply
''')
print(s(keys=['items_with_property'], args=item_ids))

Примечание: альтернативным подходом может быть использование временного ключа для хранения элементов, предоставленных пользователем, а затем выполнение операции SINTER над исходным и временным наборами. Для больших локальных наборов это должно работать лучше.

Благодарю за ваш ответ. Я буду использовать ваш сценарий в качестве отправной точки для своих экспериментов.

ErhoSen 04.02.2019 19:39

Я обновил свой ответ тестом производительности, это может быть вам интересно. Еще раз спасибо.

ErhoSen 05.02.2019 09:53

Другие вопросы по теме