package io.rebloom.client; import redis.clients.jedis.Connection; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import redis.clients.jedis.Protocol; import redis.clients.jedis.commands.ProtocolCommand; import redis.clients.jedis.exceptions.JedisException; import redis.clients.jedis.util.Pool; import redis.clients.jedis.util.SafeEncoder; import java.io.Closeable; import java.util.*; import java.util.stream.Collectors; /** * Client is the main ReBloom client class, wrapping connection management and all ReBloom commands */ public class Client implements Closeable { private final Pool<Jedis> pool; /** * Create a new client to ReBloom * @param pool Jedis connection pool to be used */ public Client(Pool<Jedis> pool){ this.pool = pool; } /** * Create a new client to ReBloom * @param host the redis host * @param port the redis port * @param timeout connection timeout * @param poolSize the poolSize of JedisPool */ public Client(String host, int port, int timeout, int poolSize) { JedisPoolConfig conf = new JedisPoolConfig(); conf.setMaxTotal(poolSize); conf.setTestOnBorrow(false); conf.setTestOnReturn(false); conf.setTestOnCreate(false); conf.setTestWhileIdle(false); conf.setMinEvictableIdleTimeMillis(60000); conf.setTimeBetweenEvictionRunsMillis(30000); conf.setNumTestsPerEvictionRun(-1); conf.setFairness(true); pool = new JedisPool(conf, host, port, timeout); } /** * Create a new client to ReBloom * @param host the redis host * @param port the redis port */ public Client(String host, int port) { this(host, port, 500, 100); } /** * Reserve a bloom filter. * @param name The key of the filter * @param initCapacity Optimize for this many items * @param errorRate The desired rate of false positives * * Note that if a filter is not reserved, a new one is created when {@link #add(String, byte[])} * is called. */ public void createFilter(String name, long initCapacity, double errorRate) { try (Jedis conn = _conn()) { String rep = sendCommand(conn, Command.RESERVE, SafeEncoder.encode(name), Protocol.toByteArray(errorRate), Protocol.toByteArray(initCapacity)).getStatusCodeReply(); if (!rep.equals("OK")) { throw new JedisException(rep); } } } /** * Adds an item to the filter * @param name The name of the filter * @param value The value to add to the filter * @return true if the item was not previously in the filter. */ public boolean add(String name, String value) { try (Jedis conn = _conn()) { return sendCommand(conn, Command.ADD, SafeEncoder.encode(name), SafeEncoder.encode(value)).getIntegerReply() != 0; } } /** * Like {@link #add(String, String)}, but allows you to store non-string items * @param name Name of the filter * @param value Value to add to the filter * @return true if the item was not previously in the filter */ public boolean add(String name, byte[] value) { try (Jedis conn = _conn()) { return sendCommand(conn, Command.ADD, SafeEncoder.encode(name), value).getIntegerReply() != 0; } } @SafeVarargs private final boolean[] sendMultiCommand(Command cmd, byte[] name, byte[]... values) { byte[][] args = new byte[values.length + 1][]; args[0] = name; System.arraycopy(values, 0, args, 1, values.length); List<Long> reps; try (Jedis conn = _conn()) { reps = sendCommand(conn, cmd, args).getIntegerMultiBulkReply(); } boolean[] ret = new boolean[values.length]; for (int i = 0; i < reps.size(); i++) { ret[i] = reps.get(i) != 0; } return ret; } /** * Add one or more items to a filter * @param name Name of the filter * @param values values to add to the filter. * @return An array of booleans of the same length as the number of values. * Each boolean values indicates whether the corresponding element was previously in the * filter or not. A true value means the item did not previously exist, whereas a * false value means it may have previously existed. * * @see #add(String, String) */ public boolean[] addMulti(String name, byte[] ...values) { return sendMultiCommand(Command.MADD, SafeEncoder.encode(name), values); } public boolean[] addMulti(String name, String ...values) { return sendMultiCommand(Command.MADD, SafeEncoder.encode(name), SafeEncoder.encodeMany(values)); } /** * Check if an item exists in the filter * @param name Name (key) of the filter * @param value Value to check for * @return true if the item may exist in the filter, false if the item does not exist in the filter */ public boolean exists(String name, String value) { try (Jedis conn = _conn()) { return sendCommand(conn, Command.EXISTS, SafeEncoder.encode(name), SafeEncoder.encode(value)).getIntegerReply() != 0; } } /** * Check if an item exists in the filter. Similar to {@link #exists(String, String)} * @param name Key of the filter to check * @param value Value to check for * @return true if the item may exist in the filter, false if the item does not exist in the filter. */ public boolean exists(String name, byte[] value) { try (Jedis conn = _conn()) { return sendCommand(conn, Command.EXISTS, SafeEncoder.encode(name), value).getIntegerReply() != 0; } } /** * Check if one or more items exist in the filter * @param name Name of the filter to check * @param values values to check for * @return An array of booleans. A true value means the corresponding value may exist, false means it does not exist */ public boolean[] existsMulti(String name, byte[] ...values) { return sendMultiCommand(Command.MEXISTS, SafeEncoder.encode(name), values); } public boolean[] existsMulti(String name, String ...values) { return sendMultiCommand(Command.MEXISTS, SafeEncoder.encode(name), SafeEncoder.encodeMany(values)); } /** * Remove the filter * @param name * @return true if delete the filter, false is not delete the filter */ public boolean delete(String name) { try(Jedis conn = _conn()){ return conn.del(name) != 0; } } /** * TOPK.RESERVE key topk width depth decay * * Reserve a topk filter. * @param key The key of the filter * @param topk * @param width * @param depth * @param decay * * Note that if a filter is not reserved, a new one is created when {@link #add(String, byte[])} * is called. */ public void topkCreateFilter(String key, long topk, long width, long depth, double decay) { try (Jedis conn = _conn()) { String rep = sendCommand(conn, TopKCommand.RESERVE, SafeEncoder.encode(key), Protocol.toByteArray(topk), Protocol.toByteArray(width), Protocol.toByteArray(depth),Protocol.toByteArray(decay)) .getStatusCodeReply(); if (!rep.equals("OK")) { throw new JedisException(rep); } } } /** * TOPK.ADD key item [item ...] * * Adds an item to the filter * @param key The key of the filter * @param items The items to add to the filter * @return list of items dropped from the list. */ public List<String> topkAdd(String key, String ...items) { try (Jedis conn = _conn()) { return sendCommand(conn, key, TopKCommand.ADD, items).getMultiBulkReply(); } } /** * TOPK.INCRBY key item increment [item increment ...] * * Adds an item to the filter * @param key The key of the filter * @param item The item to increment * @return item dropped from the list. */ public String topkIncrBy(String key, String item, long increment) { try (Jedis conn = _conn()) { return sendCommand(conn, TopKCommand.INCRBY, SafeEncoder.encode(key), SafeEncoder.encode(item), Protocol.toByteArray(increment)) .getMultiBulkReply().get(0); } } /** * TOPK.QUERY key item [item ...] * * Checks whether an item is one of Top-K items. * * @param key The key of the filter * @param items The items to check in the list * @return list of indicator for each item requested */ public List<Boolean> topkQuery(String key, String ...items) { try (Jedis conn = _conn()) { return sendCommand(conn, key, TopKCommand.QUERY, items) .getIntegerMultiBulkReply() .stream().map(s -> s!=0) .collect(Collectors.toList()); } } /** * TOPK.COUNT key item [item ...] * * Returns count for an item. * * @param key The key of the filter * @param items The items to check in the list * @return list of counters per item. */ public List<Long> topkCount(String key, String ...items) { try (Jedis conn = _conn()) { return sendCommand(conn, key, TopKCommand.COUNT, items) .getIntegerMultiBulkReply(); } } /** * TOPK.LIST key * * Return full list of items in Top K list. * * @param key The key of the filter * @return list of items in the list. */ public List<String> topkList(String key) { try (Jedis conn = _conn()) { return sendCommand(conn, TopKCommand.LIST, SafeEncoder.encode(key)) .getMultiBulkReply(); } } @Override public void close(){ this.pool.close(); } Jedis _conn() { return pool.getResource(); } private Connection sendCommand(Jedis conn, String key, ProtocolCommand command, String ...args) { byte[][] fullArgs = new byte[args.length + 1][]; fullArgs[0] = SafeEncoder.encode(key); System.arraycopy( SafeEncoder.encodeMany(args), 0, fullArgs, 1, args.length); return sendCommand(conn, command, fullArgs); } private Connection sendCommand(Jedis conn, ProtocolCommand command, byte[]... args) { Connection client = conn.getClient(); client.sendCommand(command, args); return client; } }