55
66from redisvl .query .filter import FilterExpression
77from redisvl .redis .utils import array_to_buffer
8+ from redisvl .utils .log import get_logger
89from redisvl .utils .token_escaper import TokenEscaper
910from redisvl .utils .utils import denorm_cosine_distance , lazy_import
1011
12+ logger = get_logger (__name__ )
13+
1114nltk = lazy_import ("nltk" )
1215nltk_stopwords = lazy_import ("nltk.corpus.stopwords" )
1316
17+ # Type alias for sort specification
18+ # Can be:
19+ # - str: single field name (ASC by default)
20+ # - Tuple[str, str]: (field_name, direction)
21+ # - List: list of field names or tuples
22+ SortSpec = Union [str , Tuple [str , str ], List [Union [str , Tuple [str , str ]]]]
23+
1424
1525class BaseQuery (RedisQuery ):
1626 """
@@ -58,6 +68,144 @@ def _build_query_string(self) -> str:
5868 """Build the full Redis query string."""
5969 raise NotImplementedError ("Must be implemented by subclasses" )
6070
71+ @staticmethod
72+ def _parse_sort_spec (sort_spec : Optional [SortSpec ]) -> List [Tuple [str , bool ]]:
73+ """Parse sort specification into list of (field, ascending) tuples.
74+
75+ Args:
76+ sort_spec: Sort specification in various formats:
77+ - str: single field name (defaults to ASC)
78+ - Tuple[str, str]: (field_name, "ASC"|"DESC")
79+ - List: list of strings or tuples
80+
81+ Returns:
82+ List of (field_name, ascending) tuples where ascending is a boolean.
83+
84+ Raises:
85+ TypeError: If sort_spec is not a valid type.
86+ ValueError: If direction is not "ASC" or "DESC".
87+
88+ Examples:
89+ >>> BaseQuery._parse_sort_spec("price")
90+ [("price", True)]
91+ >>> BaseQuery._parse_sort_spec(("price", "DESC"))
92+ [("price", False)]
93+ >>> BaseQuery._parse_sort_spec(["price", ("rating", "DESC")])
94+ [("price", True), ("rating", False)]
95+ """
96+ if sort_spec is None or sort_spec == []:
97+ return []
98+
99+ result : List [Tuple [str , bool ]] = []
100+
101+ # Single field as string
102+ if isinstance (sort_spec , str ):
103+ result .append ((sort_spec , True )) # Default to ASC
104+
105+ # Single field as tuple
106+ elif isinstance (sort_spec , tuple ):
107+ if len (sort_spec ) != 2 :
108+ raise ValueError (
109+ f"Sort tuple must have exactly 2 elements (field, direction), got { len (sort_spec )} "
110+ )
111+ field , direction = sort_spec
112+ if not isinstance (field , str ):
113+ raise TypeError (f"Field name must be a string, got { type (field )} " )
114+ if not isinstance (direction , str ):
115+ raise TypeError (f"Direction must be a string, got { type (direction )} " )
116+
117+ direction_upper = direction .upper ()
118+ if direction_upper not in ("ASC" , "DESC" ):
119+ raise ValueError (
120+ f"Sort direction must be 'ASC' or 'DESC', got '{ direction } '"
121+ )
122+
123+ result .append ((field , direction_upper == "ASC" ))
124+
125+ # Multiple fields as list
126+ elif isinstance (sort_spec , list ):
127+ for item in sort_spec :
128+ # Recursively parse each item
129+ parsed = BaseQuery ._parse_sort_spec (item )
130+ result .extend (parsed )
131+
132+ else :
133+ raise TypeError (
134+ f"sort_by must be a string, tuple, or list, got { type (sort_spec )} "
135+ )
136+
137+ return result
138+
139+ def sort_by (
140+ self , sort_spec : Optional [SortSpec ] = None , asc : bool = True
141+ ) -> "BaseQuery" :
142+ """Set the sort order for query results.
143+
144+ This method supports sorting by single or multiple fields. Note that Redis Search
145+ natively supports only a single SORTBY field. When multiple fields are specified,
146+ only the FIRST field is used for the Redis SORTBY clause.
147+
148+ Args:
149+ sort_spec: Sort specification in various formats:
150+ - str: single field name
151+ - Tuple[str, str]: (field_name, "ASC"|"DESC")
152+ - List: list of field names or tuples
153+ asc: Default sort direction when not specified (only used when sort_spec is a string).
154+ Defaults to True (ascending).
155+
156+ Returns:
157+ self: Returns the query object for method chaining.
158+
159+ Raises:
160+ TypeError: If sort_spec is not a valid type.
161+ ValueError: If direction is not "ASC" or "DESC".
162+
163+ Examples:
164+ >>> query.sort_by("price") # Single field, ascending
165+ >>> query.sort_by(("price", "DESC")) # Single field, descending
166+ >>> query.sort_by(["price", "rating"]) # Multiple fields (only first used)
167+ >>> query.sort_by([("price", "DESC"), ("rating", "ASC")])
168+
169+ Note:
170+ When multiple fields are specified, only the first field is used for sorting
171+ in Redis. Future versions may support multi-field sorting through post-query
172+ sorting in Python.
173+ """
174+ if sort_spec is None or sort_spec == []:
175+ # No sorting
176+ self ._sortby = None
177+ return self
178+
179+ # Handle backward compatibility: if sort_spec is a string and asc is specified
180+ # treat it as the old (field, asc) format
181+ parsed : List [Tuple [str , bool ]]
182+ if isinstance (sort_spec , str ) and asc is not True :
183+ # Old API: query.sort_by("field", asc=False)
184+ parsed = [(sort_spec , asc )]
185+ else :
186+ # New API: parse the sort_spec
187+ parsed = self ._parse_sort_spec (sort_spec )
188+
189+ if not parsed :
190+ self ._sortby = None
191+ return self
192+
193+ # Use the first field for Redis SORTBY
194+ first_field , first_asc = parsed [0 ]
195+
196+ # Log warning if multiple fields specified
197+ if len (parsed ) > 1 :
198+ logger .warning (
199+ f"Multiple sort fields specified: { [f [0 ] for f in parsed ]} . "
200+ f"Redis Search only supports single-field sorting. Using first field: '{ first_field } '. "
201+ "Additional fields are ignored."
202+ )
203+
204+ # Call parent's sort_by with the first field
205+ super ().sort_by (first_field , asc = first_asc )
206+
207+ return self
208+
61209 def set_filter (
62210 self , filter_expression : Optional [Union [str , FilterExpression ]] = None
63211 ):
@@ -170,7 +318,7 @@ def __init__(
170318 return_fields : Optional [List [str ]] = None ,
171319 num_results : int = 10 ,
172320 dialect : int = 2 ,
173- sort_by : Optional [str ] = None ,
321+ sort_by : Optional [SortSpec ] = None ,
174322 in_order : bool = False ,
175323 params : Optional [Dict [str , Any ]] = None ,
176324 ):
@@ -182,7 +330,12 @@ def __init__(
182330 return_fields (Optional[List[str]], optional): The fields to return.
183331 num_results (Optional[int], optional): The number of results to return. Defaults to 10.
184332 dialect (int, optional): The query dialect. Defaults to 2.
185- sort_by (Optional[str], optional): The field to order the results by. Defaults to None.
333+ sort_by (Optional[SortSpec], optional): The field(s) to order the results by. Can be:
334+ - str: single field name (e.g., "price")
335+ - Tuple[str, str]: (field_name, "ASC"|"DESC") (e.g., ("price", "DESC"))
336+ - List: list of fields or tuples (e.g., ["price", ("rating", "DESC")])
337+ Note: Redis Search only supports single-field sorting, so only the first field is used.
338+ Defaults to None.
186339 in_order (bool, optional): Requires the terms in the field to have the same order as the
187340 terms in the query filter. Defaults to False.
188341 params (Optional[Dict[str, Any]], optional): The parameters for the query. Defaults to None.
@@ -292,7 +445,7 @@ def __init__(
292445 num_results : int = 10 ,
293446 return_score : bool = True ,
294447 dialect : int = 2 ,
295- sort_by : Optional [str ] = None ,
448+ sort_by : Optional [SortSpec ] = None ,
296449 in_order : bool = False ,
297450 hybrid_policy : Optional [str ] = None ,
298451 batch_size : Optional [int ] = None ,
@@ -319,8 +472,12 @@ def __init__(
319472 distance. Defaults to True.
320473 dialect (int, optional): The RediSearch query dialect.
321474 Defaults to 2.
322- sort_by (Optional[str]): The field to order the results by. Defaults
323- to None. Results will be ordered by vector distance.
475+ sort_by (Optional[SortSpec]): The field(s) to order the results by. Can be:
476+ - str: single field name
477+ - Tuple[str, str]: (field_name, "ASC"|"DESC")
478+ - List: list of fields or tuples
479+ Note: Only the first field is used for Redis sorting.
480+ Defaults to None. Results will be ordered by vector distance.
324481 in_order (bool): Requires the terms in the field to have
325482 the same order as the terms in the query filter, regardless of
326483 the offsets between them. Defaults to False.
@@ -543,7 +700,7 @@ def __init__(
543700 num_results : int = 10 ,
544701 return_score : bool = True ,
545702 dialect : int = 2 ,
546- sort_by : Optional [str ] = None ,
703+ sort_by : Optional [SortSpec ] = None ,
547704 in_order : bool = False ,
548705 hybrid_policy : Optional [str ] = None ,
549706 batch_size : Optional [int ] = None ,
@@ -576,8 +733,12 @@ def __init__(
576733 distance. Defaults to True.
577734 dialect (int, optional): The RediSearch query dialect.
578735 Defaults to 2.
579- sort_by (Optional[str]): The field to order the results by. Defaults
580- to None. Results will be ordered by vector distance.
736+ sort_by (Optional[SortSpec]): The field(s) to order the results by. Can be:
737+ - str: single field name
738+ - Tuple[str, str]: (field_name, "ASC"|"DESC")
739+ - List: list of fields or tuples
740+ Note: Only the first field is used for Redis sorting.
741+ Defaults to None. Results will be ordered by vector distance.
581742 in_order (bool): Requires the terms in the field to have
582743 the same order as the terms in the query filter, regardless of
583744 the offsets between them. Defaults to False.
@@ -863,7 +1024,7 @@ def __init__(
8631024 num_results : int = 10 ,
8641025 return_score : bool = True ,
8651026 dialect : int = 2 ,
866- sort_by : Optional [str ] = None ,
1027+ sort_by : Optional [SortSpec ] = None ,
8671028 in_order : bool = False ,
8681029 params : Optional [Dict [str , Any ]] = None ,
8691030 stopwords : Optional [Union [str , Set [str ]]] = "english" ,
@@ -887,8 +1048,12 @@ def __init__(
8871048 Defaults to True.
8881049 dialect (int, optional): The RediSearch query dialect.
8891050 Defaults to 2.
890- sort_by (Optional[str]): The field to order the results by. Defaults
891- to None. Results will be ordered by text score.
1051+ sort_by (Optional[SortSpec]): The field(s) to order the results by. Can be:
1052+ - str: single field name
1053+ - Tuple[str, str]: (field_name, "ASC"|"DESC")
1054+ - List: list of fields or tuples
1055+ Note: Only the first field is used for Redis sorting.
1056+ Defaults to None. Results will be ordered by text score.
8921057 in_order (bool): Requires the terms in the field to have
8931058 the same order as the terms in the query filter, regardless of
8941059 the offsets between them. Defaults to False.
0 commit comments