Поиск элементов в Hazelcast Map с помощью ValueExtractor

Hazelcast предоставляет разработчику довольно удобные и мощные средства поиска элементов в IMap, называемые Distributed Queries. К сожалению официальная документация описывает только базовые варианты использования и совсем мало информации о возможностях Value Extractors.

В данной статье постараюсь описать свой опыт использования Value Exctractors для решения задач поиска а IMap, когда значением в IMap является коллекция объектов и нам нужно найти запись в IMap у которой есть заданный элемент в коллекции.

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

IMap<String, List> bucket; //key - имя пользователя, value - список товаров.

bucket.put("Mikhail", Arrays.asList("iphone", "ibook"));
bucket.put("Archi", Arrays.asList("iphone", "ipizza"));
bucket.put("Anna", Arrays.asList("imirror", "iphone"));

И мы хотим найти всех пользователей, у которых в корзине есть iphone.
Для этого Hazelcast предоставляет механизм называемый ValueExtractor, данный механизм является распределенным, то есть данные обрабатываются для каждого узла независимо, что эффективнее с точки зрения производительности, чем фильтрация данных на клиенте.

Чтобы воспользоваться Value Extractor нужно сделать 2 вещи:

  1. Описать класс расширяющий ValueExtractor, который будет реализовывать логику обработки и извлечения нужных данных из значения записи в IMap, в нашем случае из списка товаров.
  2. Зарегистрировать данный класс в конфигурации для IMap как custom attribute.

Далее станет возможным использовать данный value extractor в предикатах для поиска данных в IMap.

Итак создадим класс и реализуем нужную нам логику извлечения данных в методе extract:

public class BucketGoodsValueExtractor extends ValueExtractor<List<String>, String> {
    @Override
    public void extract(List<String> goods, String arg, ValueCollector valueCollector) {
        if ("any".equalsIgnoreCase(arg)) {
            for (String item : goods) {
                valueCollector.addObject(item);
            }
        }
    }
}

Метод extract принимает три параметра:

Первый параметр – это само значение записи (entry.value) в IMap, которое hazelcast передаст в этот метод для каждого ключа. В нашем случае это список товаров в корзине у пользователя.

Второй параметр – это extraction argument который можно задать в предикате в квадратных скобках и использовать его в value extractor для дополнительной гибкости. В нашем случае с его помощью задается критерий, что любой элемент списка может удовлетворять предикату. Кроме этого мы можем реализовать и другие критерии, например, первый или последний элемент и так далее:

public class BucketGoodsValueExtractor extends ValueExtractor<List<String>, String> {
    @Override
    public void extract(List<String> goods, String arg, ValueCollector valueCollector) {
        if ("first".equalsIgnoreCase(arg)) {
            if (goods.size() > 0) {
                valueCollector.addObject(goods.get(0));
            }
        } else if ("last".equalsIgnoreCase(arg)) {
            if (goods.size() > 0) {
                valueCollector.addObject(goods.get(goods.size() - 1));
            }
        } else if ("any".equalsIgnoreCase(arg)) {
            for (String item : goods) {
                valueCollector.addObject(item);
            }
        }
    }
}

Третий параметр метода extract – это value collector, контейнер для результатов, для которого будет применяться сам предикат. В нашем случае мы помещаем туда нужные нам элементы из списка товаров, в зависимости от критерия: первый, последний или все элементы списка.

Зарегистрируем custom attribute с именем “bucketGoods” и нашим value extractor’ом :

Config cfg = new Config();
MapConfig bucketMapConfig = new MapConfig("bucket");
bucketMapConfig.addMapAttributeConfig(new MapAttributeConfig("bucketGoods", BucketGoodsValueExtractor.class.getName()));
cfg.addMapConfig(bucketMapConfig);
HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance(cfg);

Здесь указан пример конфигурации из исходно кода, кроме того можно воспользоваться xml конфигурацией, подробности можно найти в документации.

Теперь добавим для примера нескольких пользователей:

IMap<String, List<String>> bucket = hazelcast.getMap("bucket");

bucket.put("Mikhail", Arrays.asList("iphone", "ibook"));
bucket.put("Archi", Arrays.asList("ibottle", "ipizza"));
bucket.put("Anna", Arrays.asList("imirror", "iphone"));

И вот, мы можем найти пользователей у кого в корзине есть “iphone”

Predicate withIPhone = Predicates.equal("bucketGoods[any]", "iphone");

List<String> usersWithIPhone = bucket.entrySet(withIPhone).stream().map(Map.Entry::getKey).collect(Collectors.toList());
        System.out.println(usersWithIPhone); // [Anna, Mikhail]

Обратите внимание, мы задали предикат equals с нашим Value Extractor, зарегистрированным как “bucketGoods” и в квадратных скобках указали критерий “any”:

Predicates.equal("bucketGoods[any]", "iphone")

Таким же образом можем найти пользователей у кого “iphone” добавлен первым:

Predicate withIPhoneFirst = Predicates.equal("bucketGoods[first]", "iphone");

List<String> usersWithIPhoneFirst = bucket.entrySet(withIPhoneFirst).stream().map(Map.Entry::getKey).collect(Collectors.toList());
System.out.println(usersWithIPhoneFirst); // [Mikhail]

или последним:

 Predicate withIPhoneLast = Predicates.equal("bucketGoods[last]", "iphone");

List<String> usersWithIPhoneLast = bucket.entrySet(withIPhoneLast).stream().map(Map.Entry::getKey).collect(Collectors.toList());
System.out.println(usersWithIPhoneLast); // [Anna]

Теперь более сложный вариант.

Добавим цену и категорию для товаров в корзине.
Простой класс для товара в корзине:

public class Goods implements Serializable {
    private final String goodsName;
    private final int goodsPrice;
    private final String category;

    public Goods(String goodsName, int goodsPrice, String category) {
        this.goodsName = goodsName;
        this.goodsPrice = goodsPrice;
        this.category = category;
    }

    public String getGoodsName() {
        return goodsName;
    }

    public int getGoodsPrice() {
        return goodsPrice;
    }

    public String getCategory() {
        return category;
    }
}

Опишем Value Extractor, который будет извлекать сумму товаров,  для заданной с помощью extraction argument категории, либо общую если категория не задана:

public class BucketSumPriceValueExtractor extends ValueExtractor<List<Goods>, String> {
    @Override
    public void extract(List<Goods> goods, String arg, ValueCollector collector) {
        if (arg != null) {
            collector.addObject(
                    goods.stream()
                            .filter(item -> arg.equalsIgnoreCase(item.getCategory()))
                            .mapToInt(Goods::getGoodsPrice)
                            .sum()
            );
        } else {
            collector.addObject(
                    goods.stream()
                            .mapToInt(Goods::getGoodsPrice)
                            .sum()
            );
        }
    }
}

Зарегистрируем custom attribute “bucketSumPrice” и добавми несколько записей в IMap

Config cfg = new Config();
MapConfig bucketMapConfig = new MapConfig("bucket");
bucketMapConfig.addMapAttributeConfig(new MapAttributeConfig("bucketSumPrice", BucketSumPriceValueExtractor.class.getName()));
cfg.addMapConfig(bucketMapConfig);
HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance(cfg);

IMap<String, List<Goods>> bucket = hazelcast.getMap("bucket");


bucket.put("Mikhail", Arrays.asList(
        new Goods("iphone", 10, "device"),
        new Goods("ipad", 20, "device"),
        new Goods("ipizza", 2, "food")));

bucket.put("Archi", Arrays.asList(
        new Goods("ibottle", 3, "dishes"),
        new Goods("ipizza", 2, "food")));

bucket.put("Anna", Arrays.asList(
        new Goods("imirror", 5, "furniture"),
        new Goods("iphone", 10, "device")));

И выберем пользователей у которых сумма цен товаров в корзине больше 10:

Predicate totalSumGrater10 = Predicates.greaterThan("bucketSumPrice", 10);

List<String> usersWithTotalSumGrater10 = bucket.entrySet(totalSumGrater10).stream().map(Map.Entry::getKey).collect(Collectors.toList());
System.out.println(usersWithTotalSumGrater10); // [Anna, Mikhail]

Или пользователей у которых сумма цен товаров с категорией “device” больше 20:

Predicate devicesSumGrater10 = Predicates.greaterThan("bucketSumPrice[device]", 10);

List<String> usersWithDevicesSumGrater10 = bucket.entrySet(devicesSumGrater10).stream().map(Map.Entry::getKey).collect(Collectors.toList());
System.out.println(usersWithDevicesSumGrater10); // [Mikhail]

Как мы видим Value Extractor и Custom Attributes в Hazelcast предоставляют гибкий и распределенный механизм для извлечения данных, которые недоступны как атрибуты объекта, а также может быть использован для извлечения  и поиска данных в значениях, которые  являются коллекциями. Этот механизм отличная альтернатива для фильтрации данных на стороне клиента.

Полезные ссылки:

  1. Официальная документация Hazelcast
  2. Вопрос на stackoverflow, который побудил меня написать данную статью

Комментарии:

  • Константин

    спасибо! дополните, пож-та, как искать предикат по нескольким значениям, а не только больше-меньше
    Имею в виду:

    public PredicateBuilder in(Comparable… values);

    • Бакшеев Михаил

      ValueExtractor’s будут работать со всеми предикатами, в том числе и с IN. Например, можно использовать выше тот же атрибут bucketSumPrice и связаный с ним ValueExtractor в IN предикате чтобы найти записи у которых сумма в корзине 5 или 15 :

      bucket.entrySet(Predicates.in(“bucketSumPrice”, 5, 15)) //вернет записи Archi, Anna