Skip to content

fix(index): use UNLINK instead of DEL in SearchIndex.drop_keys#616

Open
Joshuaakaspace wants to merge 1 commit into
redis:mainfrom
Joshuaakaspace:fix/issue-600-drop-keys-unlink
Open

fix(index): use UNLINK instead of DEL in SearchIndex.drop_keys#616
Joshuaakaspace wants to merge 1 commit into
redis:mainfrom
Joshuaakaspace:fix/issue-600-drop-keys-unlink

Conversation

@Joshuaakaspace
Copy link
Copy Markdown

@Joshuaakaspace Joshuaakaspace commented May 14, 2026

Summary

Switch SearchIndex.drop_keys and AsyncSearchIndex.drop_keys from DEL to UNLINK so memory reclamation runs on a background thread.

Refs #600

Motivation

DEL reclaims memory on the main thread, so a single drop_keys call over a large key set stalls Redis proportionally to the freed keyspace. For SemanticCache use cases where scope-targeted invalidation routinely sweeps 10K to 1M+ keys (for example, a policy version rollover in a multi-tenant deployment), this is a customer-visible latency event on every invalidation. The issue thread on #600 lays this out in more detail, including the path through SemanticCache.drop() which calls drop_keys for the keys= argument.

UNLINK is a strict superset of DEL for this code path. It returns the same count semantics and has been available since Redis 4, which is well below the supported floor for current RedisVL targets. The only observable difference is that reclaimed memory is reported lazily by MEMORY USAGE, which is the point.

Changes

  • redisvl/index/index.py: SearchIndex.drop_keys (sync) now calls self._redis_client.unlink(...) instead of self._redis_client.delete(...). Docstring updated to note the choice.
  • redisvl/index/index.py: AsyncSearchIndex.drop_keys (async) now calls client.unlink(...) instead of client.delete(...). Docstring updated to note the choice.
  • tests/unit/test_drop_keys_unlink.py: new mock-based regression tests covering single-key and list-of-keys paths on both sync and async indexes, asserting unlink is called and delete is not.

drop_documents is intentionally left unchanged in this PR. It is a related but separate API (it also applies the index prefix and validates hash-tag co-location on cluster) and #601 is tracking the consistency story there. Keeping this PR scoped to the drop_keys change matches the framing in #600.

Testing

All run locally with python -m uv run pytest .... Docker was running for testcontainers-backed integration tests.

  • New regression suite (4 tests in tests/unit/test_drop_keys_unlink.py):
    • Fails on upstream/main with AssertionError: Expected unlink to have been [a]waited once. Called 0 times. on all 4 cases.
    • Passes on this branch.
  • Full tests/unit/: 836 passed, 1 skipped on baseline; 840 passed, 1 skipped on this branch. The +4 is the new regression suite. No previously-passing test regressed and no new skips.
  • tests/integration/test_search_index.py and tests/integration/test_async_search_index.py: 93 passed (covers the original test_search_index_drop_keys cases on both sync and async).
  • tests/integration/test_llmcache.py and tests/integration/test_semantic_router.py: 92 passed, 1 skipped. These exercise SemanticCache.drop() and SemanticRouter which transit drop_keys under the hood.
  • make-equivalent linting: isort --check-only, black --check, and mypy redisvl all clean.
Verification log tail
=== BASELINE UNIT TESTS ===
========== 836 passed, 1 skipped, 109 warnings in 250.94s (0:04:10) ===========

=== BASELINE INTEGRATION test_search_index drop_keys ===
================= 1 passed, 45 deselected, 1 warning in 6.21s =================

=== BASELINE INTEGRATION test_async_search_index drop_keys ===
================= 1 passed, 46 deselected, 1 warning in 5.59s =================

=== PATCHED: new regression test ===
tests/unit/test_drop_keys_unlink.py::TestDropKeysUsesUnlink::test_single_key_calls_unlink PASSED
tests/unit/test_drop_keys_unlink.py::TestDropKeysUsesUnlink::test_list_of_keys_calls_unlink PASSED
tests/unit/test_drop_keys_unlink.py::TestAsyncDropKeysUsesUnlink::test_single_key_calls_unlink PASSED
tests/unit/test_drop_keys_unlink.py::TestAsyncDropKeysUsesUnlink::test_list_of_keys_calls_unlink PASSED
======================== 4 passed, 1 warning in 5.07s =========================

=== format / lint (patched) ===
isort: would leave 186 files unchanged
black: 186 files would be left unchanged
mypy: Success: no issues found in 96 source files

=== PATCHED UNIT TESTS ===
========== 840 passed, 1 skipped, 108 warnings in 203.65s (0:03:23) ===========

=== PATCHED INTEGRATION drop_keys (sync + async) ===
tests/integration/test_search_index.py::test_search_index_drop_keys PASSED
tests/integration/test_async_search_index.py::test_search_index_drop_keys PASSED
================= 2 passed, 91 deselected, 1 warning in 6.47s =================

=== PATCHED INTEGRATION test_search_index + test_async_search_index (full) ===
======================= 93 passed, 1 warning in 10.75s ========================

=== PATCHED INTEGRATION llmcache + semantic_router (paths that go through drop_keys) ===
============ 92 passed, 1 skipped, 2 warnings in 372.78s (0:06:12) ============

Notes


Note

Low Risk
Low risk: a small, backwards-compatible change that swaps DEL for UNLINK to reduce Redis blocking during bulk key deletion; behavior differences are mostly limited to asynchronous memory reclamation timing.

Overview
Switches drop_keys to non-blocking deletion. SearchIndex.drop_keys and AsyncSearchIndex.drop_keys now call Redis UNLINK instead of DEL, and their docstrings document the rationale (avoid blocking Redis when dropping large key sets).

Adds a unit regression suite (tests/unit/test_drop_keys_unlink.py) asserting both sync and async drop_keys paths call unlink (single key and list) and never call delete.

Reviewed by Cursor Bugbot for commit 4fbc650. Bugbot is set up for automated code reviews on this repo. Configure here.

DEL reclaims memory on the main thread, so a single drop_keys call over
a large key set stalls Redis proportionally to the freed keyspace. For
SemanticCache use cases where scope-targeted invalidation routinely
sweeps 10K to 1M+ keys (for example, a policy version rollover in a
multi-tenant deployment), this is a customer-visible latency event on
every invalidation.

UNLINK has the same return semantics as DEL and is available on Redis
4+, so it is a strict superset for this use case. The only observable
difference is that reclaimed memory is reported lazily by MEMORY USAGE,
which is the point.

Applies to both SearchIndex.drop_keys and AsyncSearchIndex.drop_keys.
SemanticCache.drop() flows through this path via the keys= argument.

Refs redis#600
Copilot AI review requested due to automatic review settings May 14, 2026 20:27
@jit-ci
Copy link
Copy Markdown

jit-ci Bot commented May 14, 2026

Hi, I’m Jit, a friendly security platform designed to help developers build secure applications from day zero with an MVS (Minimal viable security) mindset.

In case there are security findings, they will be communicated to you as a comment inside the PR.

Hope you’ll enjoy using Jit.

Questions? Comments? Want to learn more? Get in touch with us.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR changes SearchIndex.drop_keys and AsyncSearchIndex.drop_keys to use Redis UNLINK instead of DEL, reducing main-thread blocking during large key invalidations while preserving deletion count semantics.

Changes:

  • Replaced sync and async drop_keys Redis calls from delete to unlink.
  • Updated docstrings to explain the use of UNLINK.
  • Added unit regression tests verifying unlink is used instead of delete.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
redisvl/index/index.py Switches sync and async drop_keys implementations to call UNLINK and documents the behavior.
tests/unit/test_drop_keys_unlink.py Adds mock-based tests for single-key and multi-key sync/async drop_keys paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Joshuaakaspace Joshuaakaspace marked this pull request as ready for review May 15, 2026 04:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants