Source code for redisvl.query.sql
"""SQL Query class for executing SQL-like queries against Redis."""
import re
from typing import Any
from redisvl.redis.connection import RedisConnectionFactory
[docs]
class SQLQuery:
"""A query class that translates SQL-like syntax into Redis queries.
This class allows users to write SQL SELECT statements that are
automatically translated into Redis FT.SEARCH or FT.AGGREGATE commands.
For TEXT fields with ``sql-redis >= 0.4.0``:
- ``=`` performs exact phrase or exact-term matching
- ``LIKE`` performs wildcard/pattern matching using SQL ``%`` wildcards
- ``fuzzy(field, 'term')`` performs typo-tolerant matching
- ``fulltext(field, 'query')`` performs tokenized text search
.. code-block:: python
from redisvl.query import SQLQuery
from redisvl.index import SearchIndex
index = SearchIndex.from_existing("products", redis_url="redis://localhost:6379")
sql_query = SQLQuery('''
SELECT title, price, category
FROM products
WHERE category = 'electronics' AND price < 100
''')
results = index.query(sql_query)
Note:
Requires the optional `sql-redis` package. Install with:
``pip install redisvl[sql-redis]``
"""
def __init__(
self,
sql: str,
params: dict[str, Any] | None = None,
*,
sql_redis_options: dict[str, Any] | None = None,
):
"""Initialize a SQLQuery.
Args:
sql: The SQL SELECT statement to execute.
params: Optional dictionary of parameters for parameterized queries.
Useful for passing vector data for similarity searches.
sql_redis_options: Optional passthrough options forwarded to
``sql-redis`` executor creation. Use this to tune how SQL
query translation loads and caches index schema metadata.
For example, ``{"schema_cache_strategy": "lazy"}`` loads
schemas on demand (the RedisVL default), while
``{"schema_cache_strategy": "load_all"}`` eagerly loads
all schemas up front. These options exist to balance startup
cost vs repeated-query performance across many indexes.
Note:
``sql-redis >= 0.4.0`` uses explicit TEXT search operators.
Use ``=`` for exact phrase matching, ``LIKE`` for wildcard
matching, ``fuzzy()`` for typo-tolerant matching, and
``fulltext()`` for tokenized search.
"""
self.sql = sql
self.params = params or {}
self.sql_redis_options = dict(sql_redis_options or {})
def _substitute_params(self, sql: str, params: dict[str, Any]) -> str:
"""Substitute parameter placeholders in SQL with actual values.
Uses token-based approach: splits SQL on :param patterns, then rebuilds
with substituted values. This prevents partial matching (e.g., :id
won't match inside :product_id) and is faster than regex at scale.
Args:
sql: The SQL string with :param placeholders.
params: Dictionary mapping parameter names to values.
Returns:
SQL string with parameters substituted.
Note:
- String values are wrapped in single quotes with proper escaping
- Numeric values are converted to strings
- Bytes values (e.g., vectors) are NOT substituted here
"""
if not params:
return sql
# Split SQL on :param patterns, keeping the delimiters
# Pattern matches : followed by valid identifier (letter/underscore, then alphanumeric/underscore)
tokens = re.split(r"(:[a-zA-Z_][a-zA-Z0-9_]*)", sql)
result = []
for token in tokens:
if token.startswith(":"):
key = token[1:] # Remove leading :
if key in params:
value = params[key]
if isinstance(value, (int, float)):
result.append(str(value))
elif isinstance(value, str):
# Escape single quotes using SQL standard: ' -> ''
escaped = value.replace("'", "''")
result.append(f"'{escaped}'")
else:
# Keep placeholder for bytes (vectors handled by Executor)
result.append(token)
else:
# Keep unmatched placeholders as-is
result.append(token)
else:
result.append(token)
return "".join(result)
[docs]
def redis_query_string(
self,
redis_client: Any | None = None,
redis_url: str = "redis://localhost:6379",
) -> str:
"""Translate the SQL query to a Redis command string.
This method uses the sql-redis translator to convert the SQL statement
into the equivalent Redis FT.SEARCH or FT.AGGREGATE command.
Args:
redis_client: A Redis client connection used to load index schemas.
If not provided, a connection will be created using redis_url.
redis_url: The Redis URL to connect to if redis_client is not provided.
Defaults to "redis://localhost:6379".
Returns:
The Redis command string (e.g., 'FT.SEARCH products "@category:{electronics}"').
Raises:
ImportError: If sql-redis package is not installed.
Example:
.. code-block:: python
from redisvl.query import SQLQuery
sql_query = SQLQuery("SELECT * FROM products WHERE category = 'electronics'")
# Using redis_url
redis_cmd = sql_query.redis_query_string(redis_url="redis://localhost:6379")
# Or using an existing client
from redis import Redis
client = Redis()
redis_cmd = sql_query.redis_query_string(redis_client=client)
print(redis_cmd)
# Output: FT.SEARCH products "@category:{electronics}"
"""
try:
from sql_redis import create_executor
except ImportError:
raise ImportError(
"sql-redis is required for SQL query support. "
"Install it with: pip install redisvl[sql-redis]"
)
# Get or create Redis client
if redis_client is None:
redis_client = RedisConnectionFactory.get_redis_connection(
redis_url=redis_url
)
sql_redis_options = {
"schema_cache_strategy": "lazy",
**self.sql_redis_options,
}
executor = create_executor(redis_client, **sql_redis_options)
# Substitute non-bytes params in SQL before translation
sql = self._substitute_params(self.sql, self.params)
translated = executor._translator.translate(sql)
return translated.to_command_string()