diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java index dd02f85661..0e86a7deef 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -1146,6 +1146,20 @@ default Long zInterStore(byte[] destKey, byte[]... sets) { return zSetCommands().zInterStore(destKey, sets); } + /** @deprecated in favor of {@link RedisConnection#zSetCommands()}}. */ + @Override + @Deprecated + default Long zInterCard(byte[]... sets) { + return zSetCommands().zInterCard(sets); + } + + /** @deprecated in favor of {@link RedisConnection#zSetCommands()}}. */ + @Override + @Deprecated + default Long zInterCard(long limit, byte[]... sets) { + return zSetCommands().zInterCard(limit, sets); + } + /** @deprecated in favor of {@link RedisConnection#zSetCommands()}}. */ @Override @Deprecated diff --git a/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java index 2518918b09..716bf6ccc2 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java @@ -42,6 +42,7 @@ * @author Mark Paluch * @author Andrey Shlykov * @author Shyngys Sapraliyev + * @author GyeongHoe Koo * @see RedisCommands */ @NullUnmarked @@ -1085,6 +1086,28 @@ default Long zInterStore(byte @NonNull [] destKey, @NonNull Aggregate aggregate, Long zInterStore(byte @NonNull [] destKey, @NonNull Aggregate aggregate, @NonNull Weights weights, byte @NonNull [] @NonNull... sets); + /** + * Get the number of members in the intersection of sorted {@code sets}. + * + * @param sets must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 4.0 + * @see Redis Documentation: ZINTERCARD + */ + Long zInterCard(byte @NonNull [] @NonNull... sets); + + /** + * Get the number of members in the intersection of sorted {@code sets}. + * + * @param sets must not be {@literal null}. + * @param limit the maximum cardinality to compute. If the intersection has more than {@code limit} elements, + * the computation stops and returns {@code limit}. + * @return {@literal null} when used in pipeline / transaction. + * @since 4.0 + * @see Redis Documentation: ZINTERCARD + */ + Long zInterCard(long limit, byte @NonNull [] @NonNull... sets); + /** * Union sorted {@code sets}. * diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterZSetCommands.java index 352230a8b6..0c2990b6ea 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterZSetCommands.java @@ -1086,6 +1086,42 @@ public Long zUnionStore(byte @NonNull [] destKey, @NonNull Aggregate aggregate, throw new InvalidDataAccessApiUsageException("ZUNIONSTORE can only be executed when all keys map to the same slot"); } + @Override + public Long zInterCard(byte @NonNull [] @NonNull... sets) { + + Assert.notNull(sets, "Sets must not be null"); + Assert.noNullElements(sets, "Sets must not contain null elements"); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(sets)) { + + try { + return connection.getCluster().zintercard(sets); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + throw new InvalidDataAccessApiUsageException("ZINTERCARD can only be executed when all keys map to the same slot"); + } + + @Override + public Long zInterCard(long limit, byte @NonNull [] @NonNull... sets) { + + Assert.notNull(sets, "Sets must not be null"); + Assert.noNullElements(sets, "Sets must not contain null elements"); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(sets)) { + + try { + return connection.getCluster().zintercard(limit, sets); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + throw new InvalidDataAccessApiUsageException("ZINTERCARD can only be executed when all keys map to the same slot"); + } + @Override public Cursor<@NonNull Tuple> zScan(byte @NonNull [] key, @NonNull ScanOptions options) { diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisZSetCommands.java index a90cfbde86..b7d6c63600 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisZSetCommands.java @@ -564,6 +564,24 @@ public Long zUnionStore(byte @NonNull [] destKey, byte @NonNull [] @NonNull... s return connection.invoke().just(Jedis::zunionstore, PipelineBinaryCommands::zunionstore, destKey, sets); } + @Override + public Long zInterCard(byte @NonNull [] @NonNull... sets) { + + Assert.notNull(sets, "Sets must not be null"); + Assert.noNullElements(sets, "Sets must not contain null elements"); + + return connection.invoke().just(Jedis::zintercard, PipelineBinaryCommands::zintercard, sets); + } + + @Override + public Long zInterCard(long limit, byte @NonNull [] @NonNull... sets) { + + Assert.notNull(sets, "Sets must not be null"); + Assert.noNullElements(sets, "Sets must not contain null elements"); + + return connection.invoke().just(Jedis::zintercard, PipelineBinaryCommands::zintercard, limit, sets); + } + @Override public Cursor<@NonNull Tuple> zScan(byte @NonNull [] key, @NonNull ScanOptions options) { return zScan(key, CursorId.initial(), options); diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceZSetCommands.java index e1a02dff78..15b96e9cc0 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceZSetCommands.java @@ -532,6 +532,24 @@ public Long zUnionStore(byte @NonNull [] destKey, byte @NonNull [] @NonNull... s return connection.invoke().just(RedisSortedSetAsyncCommands::zunionstore, destKey, sets); } + @Override + public Long zInterCard(byte @NonNull [] @NonNull... sets) { + + Assert.notNull(sets, "Sets must not be null"); + Assert.noNullElements(sets, "Sets must not contain null elements"); + + return connection.invoke().just(RedisSortedSetAsyncCommands::zintercard, sets); + } + + @Override + public Long zInterCard(long limit, byte @NonNull [] @NonNull... sets) { + + Assert.notNull(sets, "Sets must not be null"); + Assert.noNullElements(sets, "Sets must not contain null elements"); + + return connection.invoke().just(RedisSortedSetAsyncCommands::zintercard, limit, sets); + } + @Override public Cursor<@NonNull Tuple> zScan(byte @NonNull [] key, @Nullable ScanOptions options) { return zScan(key, CursorId.initial(), options != null ? options : ScanOptions.NONE); diff --git a/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java index be4573e890..70057c4b0c 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java @@ -46,6 +46,7 @@ * @author Shyngys Sapraliyev * @author John Blum * @author Gunha Hwang + * @author GyeongHoe Koo */ @NullUnmarked class DefaultZSetOperations extends AbstractOperations implements ZSetOperations { @@ -588,6 +589,22 @@ public Long intersectAndStore(@NonNull K key, Collection<@NonNull K> otherKeys, return execute(connection -> connection.zInterStore(rawDestKey, aggregate, weights, rawKeys)); } + @Override + public Long intersectSize(@NonNull K key, @NonNull Collection<@NonNull K> otherKeys) { + + byte[][] rawKeys = rawKeys(key, otherKeys); + + return execute(connection -> connection.zInterCard(rawKeys)); + } + + @Override + public Long intersectSize(@NonNull K key, @NonNull Collection<@NonNull K> otherKeys, long limit) { + + byte[][] rawKeys = rawKeys(key, otherKeys); + + return execute(connection -> connection.zInterCard(limit, rawKeys)); + } + @Override public Set union(@NonNull K key, @NonNull Collection<@NonNull K> otherKeys) { diff --git a/src/main/java/org/springframework/data/redis/core/ZSetOperations.java b/src/main/java/org/springframework/data/redis/core/ZSetOperations.java index c5a640bb87..fd906fbcbe 100644 --- a/src/main/java/org/springframework/data/redis/core/ZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ZSetOperations.java @@ -44,6 +44,7 @@ * @author Andrey Shlykov * @author Shyngys Sapraliyev * @author Gunha Hwang + * @author GyeongHoe Koo */ @NullUnmarked public interface ZSetOperations { @@ -766,6 +767,40 @@ default Long intersectAndStore(@NonNull K key, @NonNull Collection<@NonNull K> o Long intersectAndStore(@NonNull K key, @NonNull Collection<@NonNull K> otherKeys, @NonNull K destKey, @NonNull Aggregate aggregate, @NonNull Weights weights); + /** + * Returns numbers of members in the sorted set resulting from the intersection of the sorted sets stored at {@code key} and {@code otherKey}. + * + * @param key must not be {@literal null}. + * @param otherKey must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: ZINTERCARD + */ + default Long intersectSize(@NonNull K key, @NonNull K otherKey) { + return intersectSize(key, Collections.singleton(otherKey)); + } + + /** + * Returns numbers of members in the sorted set resulting from the intersection of the sorted sets stored at {@code key} and {@code otherKey}. + * + * @param key must not be {@literal null}. + * @param otherKeys must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: ZINTERCARD + */ + Long intersectSize(@NonNull K key, @NonNull Collection<@NonNull K> otherKeys); + + /** + * Returns numbers of members in the sorted set resulting from the intersection of the sorted sets stored at {@code key} and {@code otherKey}. + * + * @param key must not be {@literal null}. + * @param otherKeys must not be {@literal null}. + * @param limit the maximum cardinality to compute. If the intersection has more than {@code limit} elements, + * the computation stops and returns {@code limit}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: ZINTERCARD + */ + Long intersectSize(@NonNull K key, @NonNull Collection<@NonNull K> otherKeys, long limit); + /** * Union sorted {@code sets}. * diff --git a/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsIntegrationTests.java index 0fe579b153..f141bf5e78 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsIntegrationTests.java @@ -24,6 +24,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -608,6 +609,55 @@ void testZsetIntersectWithAggregateWeights() { assertThat(zSetOps.score(key1, value1)).isCloseTo(6.0, offset(0.1)); } + @Test // GH-3253 + @EnabledOnCommand("ZINTERCARD") + void testZsetIntersectSize() { + + K key1 = keyFactory.instance(); + K key2 = keyFactory.instance(); + K key3 = keyFactory.instance(); + + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + V value3 = valueFactory.instance(); + + zSetOps.add(key1, value1, 1.0); + zSetOps.add(key1, value2, 2.0); + zSetOps.add(key1, value3, 3.0); + + zSetOps.add(key2, value2, 4.0); + zSetOps.add(key2, value3, 5.0); + + zSetOps.add(key3, value3, 6.0); + + // Test basic intersectSize + assertThat(zSetOps.intersectSize(key1, key2)).isEqualTo(2L); + assertThat(zSetOps.intersectSize(key1, Collections.singletonList(key2))).isEqualTo(2L); + + // Test with 3 sets + assertThat(zSetOps.intersectSize(key1, List.of(key2, key3))).isEqualTo(1L); + + // Test with limit + assertThat(zSetOps.intersectSize(key1, Collections.singletonList(key2), 1L)).isEqualTo(1L); + } + + @Test // GH-3253 + @EnabledOnCommand("ZINTERCARD") + void testZsetIntersectSizeEmptySet() { + + K key1 = keyFactory.instance(); + K key2 = keyFactory.instance(); + + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + zSetOps.add(key1, value1, 1.0); + zSetOps.add(key2, value2, 2.0); + + // No intersection + assertThat(zSetOps.intersectSize(key1, key2)).isEqualTo(0L); + } + @Test // GH-2042 @EnabledOnCommand("ZUNION") void testZsetUnion() {