Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/concepts/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ query = HybridQuery(

RedisVL returns query results as a list of dictionaries, one per matched document. On Redis 8.8+ a document can occasionally come back with its field payload missing: the RediSearch worker pool now defaults to a nonzero number of background worker threads, and if a document — or one of its indexed fields — expires (via TTL or `HPEXPIRE`) at the exact moment a background search is reading it, the server returns the matched id with an empty field payload instead of dropping it from the result set. This is most likely in workloads that combine search with short TTLs, such as a semantic cache or chat history.

RedisVL detects and **silently skips** such a document (logging at `WARNING` level) whenever the query type guarantees a field would be present on a healthy match:
RedisVL detects and **skips** such a document (logging at `WARNING` level) whenever the query type guarantees a field would be present on a healthy match:

- **`VectorQuery` / `VectorRangeQuery`** with `return_score=True` (the default) — a healthy match always carries a `vector_distance`.
- **`FilterQuery` on JSON storage** that returns the whole object — a healthy match always carries its JSON payload.
Expand Down
4 changes: 0 additions & 4 deletions redisvl/cli/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@
)
from redisvl.redis.utils import convert_bytes
from redisvl.schema.schema import IndexSchema
from redisvl.utils.log import get_logger

logger = get_logger("[RedisVL]")

# Exceptions commonly raised when loading or validating a schema path (-s).
SCHEMA_INPUT_ERRORS = (
Expand Down Expand Up @@ -83,7 +80,6 @@ def __init__(self):
try:
args.handler(args)
except Exception as e:
logger.error(e, exc_info=True)
print(str(e), file=sys.stderr)
sys.exit(1)

Expand Down
1 change: 0 additions & 1 deletion redisvl/cli/migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ def __init__(self):
getattr(self, command)()
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
logger.error(e)
sys.exit(1)

def helper(self):
Expand Down
4 changes: 0 additions & 4 deletions redisvl/cli/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
from redisvl.exceptions import RedisSearchError
from redisvl.index import SearchIndex
from redisvl.schema.schema import IndexSchema
from redisvl.utils.log import get_logger

logger = get_logger("[RedisVL]")

# Exceptions commonly raised when loading or validating a schema path (-s).
SCHEMA_INPUT_ERRORS = (
Expand Down Expand Up @@ -113,7 +110,6 @@ def __init__(self):
try:
self.stats(args)
except Exception as e:
logger.error(e, exc_info=True)
print(str(e), file=sys.stderr)
sys.exit(1)

Expand Down
7 changes: 1 addition & 6 deletions redisvl/utils/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,4 @@ def get_logger(name, log_level="info", fmt=None):
# Use file name if logger is in debug mode
name = "RedisVL" if log_level == "debug" else name

logger = logging.getLogger(name)

# Add a NullHandler to loggers to avoid "no handler found" warnings
if not logger.handlers:
logger.addHandler(logging.NullHandler())
return logger
return logging.getLogger(name)
31 changes: 31 additions & 0 deletions tests/unit/test_cli_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import re
import subprocess
import sys
import textwrap
from types import SimpleNamespace
from unittest.mock import MagicMock

Expand Down Expand Up @@ -459,3 +460,33 @@ def test_index_runtime_errors(
assert exc_info.value.code == 1
assert captured.out == ""
assert captured.err != ""


def test_index_failure_stderr_subprocess_has_no_traceback():
"""Unconfigured logging: ``rvl index`` failures print one line, no traceback."""
result = subprocess.run(
[
sys.executable,
"-c",
textwrap.dedent("""
import sys
from unittest.mock import MagicMock, patch

fake_index = MagicMock()
fake_index.info.side_effect = RuntimeError("boom")
with patch("redisvl.cli.index.SearchIndex", return_value=fake_index):
sys.argv = ["rvl", "index", "info", "-i", "test-idx"]
try:
from redisvl.cli.index import Index
Index()
except SystemExit as exc:
sys.exit(exc.code)
"""),
],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 1, result.stderr
assert "boom" in result.stderr
assert "Traceback" not in result.stderr
55 changes: 55 additions & 0 deletions tests/unit/test_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Logging reaches stderr when the application has not configured logging.

pytest's ``caplog`` fixture installs its own handler, so a test that uses it
passes even when ``get_logger`` swallows the record. These tests run in a
fresh interpreter and read stderr instead.
"""

import subprocess
import sys
import textwrap


def _run(script: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, "-c", textwrap.dedent(script)],
capture_output=True,
text=True,
check=False,
)


def test_warning_reaches_stderr_when_logging_is_unconfigured():
result = _run("""
import logging
from redisvl.utils.log import get_logger

get_logger("redisvl.demo").info("redisvl info hidden")
get_logger("redisvl.demo").warning("from redisvl logger")
logging.getLogger("plain.demo").warning("from plain logger")
""")
assert result.returncode == 0, result.stderr
assert "redisvl info hidden" not in result.stderr
assert "from redisvl logger" in result.stderr
assert "from plain logger" in result.stderr


def test_configured_logging_receives_warning_exactly_once():
result = _run("""
import logging
import sys

logging.basicConfig(
stream=sys.stdout,
format="APP %(levelname)s %(name)s %(message)s",
level=logging.WARNING,
)
from redisvl.utils.log import get_logger

get_logger("redisvl.demo").warning("configured redisvl warning")
""")
assert result.returncode == 0, result.stderr
assert result.stdout.splitlines() == [
"APP WARNING redisvl.demo configured redisvl warning"
]
assert "configured redisvl warning" not in result.stderr