44
55import yaml
66
7- from redisvl .migration .models import FieldUpdate , SchemaPatch , SchemaPatchChanges
7+ from redisvl .migration .models import (
8+ FieldRename ,
9+ FieldUpdate ,
10+ SchemaPatch ,
11+ SchemaPatchChanges ,
12+ )
813from redisvl .migration .planner import MigrationPlanner
914from redisvl .migration .utils import list_indexes , write_yaml
1015from redisvl .schema .schema import IndexSchema
@@ -75,6 +80,9 @@ def _load_existing_patch(self, patch_path: str) -> SchemaPatchChanges:
7580 print (f" Add fields: { len (patch .changes .add_fields )} " )
7681 print (f" Update fields: { len (patch .changes .update_fields )} " )
7782 print (f" Remove fields: { len (patch .changes .remove_fields )} " )
83+ print (f" Rename fields: { len (patch .changes .rename_fields )} " )
84+ if patch .changes .index :
85+ print (f" Index changes: { list (patch .changes .index .keys ())} " )
7886 return patch .changes
7987
8088 def _resolve_index_name (
@@ -120,8 +128,11 @@ def _build_patch(
120128 print ("1. Add field (text, tag, numeric, geo)" )
121129 print ("2. Update field (sortable, weight, separator, vector config)" )
122130 print ("3. Remove field" )
123- print ("4. Preview patch (show pending changes as YAML)" )
124- print ("5. Finish" )
131+ print ("4. Rename field (rename field in all documents)" )
132+ print ("5. Rename index (change index name)" )
133+ print ("6. Change prefix (rename all keys)" )
134+ print ("7. Preview patch (show pending changes as YAML)" )
135+ print ("8. Finish" )
125136 action = input ("Enter a number: " ).strip ()
126137
127138 if action == "1" :
@@ -137,15 +148,27 @@ def _build_patch(
137148 if field_name :
138149 changes .remove_fields .append (field_name )
139150 elif action == "4" :
151+ field_rename = self ._prompt_rename_field (source_schema )
152+ if field_rename :
153+ changes .rename_fields .append (field_rename )
154+ elif action == "5" :
155+ new_name = self ._prompt_rename_index (source_schema )
156+ if new_name :
157+ changes .index ["name" ] = new_name
158+ elif action == "6" :
159+ new_prefix = self ._prompt_change_prefix (source_schema )
160+ if new_prefix :
161+ changes .index ["prefix" ] = new_prefix
162+ elif action == "7" :
140163 print (
141164 yaml .safe_dump (
142165 {"version" : 1 , "changes" : changes .model_dump ()}, sort_keys = False
143166 )
144167 )
145- elif action == "5 " :
168+ elif action == "8 " :
146169 done = True
147170 else :
148- print ("Invalid action. Please choose 1-5 ." )
171+ print ("Invalid action. Please choose 1-8 ." )
149172
150173 return SchemaPatch (version = 1 , changes = changes )
151174
@@ -220,28 +243,125 @@ def _prompt_update_field(
220243 return FieldUpdate (name = selected ["name" ], attrs = attrs )
221244
222245 def _prompt_remove_field (self , source_schema : Dict [str , Any ]) -> Optional [str ]:
223- removable_fields = [
224- field ["name" ]
225- for field in source_schema ["fields" ]
226- if field ["type" ] != "vector"
227- ]
246+ removable_fields = [field ["name" ] for field in source_schema ["fields" ]]
228247 if not removable_fields :
229- print ("No removable Phase 1 fields are available." )
248+ print ("No fields available to remove ." )
230249 return None
231250
232251 print ("Removable fields:" )
233- for position , field_name in enumerate (removable_fields , start = 1 ):
234- print (f"{ position } . { field_name } " )
252+ for position , field in enumerate (source_schema ["fields" ], start = 1 ):
253+ field_type = field ["type" ]
254+ warning = " [WARNING: vector field]" if field_type == "vector" else ""
255+ print (f"{ position } . { field ['name' ]} ({ field_type } ){ warning } " )
235256
236257 choice = input ("Select a field to remove by number or name: " ).strip ()
258+ selected_name : Optional [str ] = None
237259 if choice in removable_fields :
238- return choice
239- if choice .isdigit ():
260+ selected_name = choice
261+ elif choice .isdigit ():
240262 offset = int (choice ) - 1
241263 if 0 <= offset < len (removable_fields ):
242- return removable_fields [offset ]
243- print ("Invalid field selection." )
244- return None
264+ selected_name = removable_fields [offset ]
265+
266+ if not selected_name :
267+ print ("Invalid field selection." )
268+ return None
269+
270+ # Check if it's a vector field and require confirmation
271+ selected_field = next (
272+ (f for f in source_schema ["fields" ] if f ["name" ] == selected_name ), None
273+ )
274+ if selected_field and selected_field ["type" ] == "vector" :
275+ print (
276+ f"\n WARNING: Removing vector field '{ selected_name } ' will:\n "
277+ " - Remove it from the search index\n "
278+ " - Leave vector data in documents (wasted storage)\n "
279+ " - Require re-embedding if you want to restore it later"
280+ )
281+ confirm = input ("Type 'yes' to confirm removal: " ).strip ().lower ()
282+ if confirm != "yes" :
283+ print ("Cancelled." )
284+ return None
285+
286+ return selected_name
287+
288+ def _prompt_rename_field (
289+ self , source_schema : Dict [str , Any ]
290+ ) -> Optional [FieldRename ]:
291+ """Prompt user to rename a field in all documents."""
292+ fields = source_schema ["fields" ]
293+ if not fields :
294+ print ("No fields available to rename." )
295+ return None
296+
297+ print ("Fields available for renaming:" )
298+ for position , field in enumerate (fields , start = 1 ):
299+ print (f"{ position } . { field ['name' ]} ({ field ['type' ]} )" )
300+
301+ choice = input ("Select a field to rename by number or name: " ).strip ()
302+ selected : Optional [Dict [str , Any ]] = None
303+ for position , field in enumerate (fields , start = 1 ):
304+ if choice == str (position ) or choice == field ["name" ]:
305+ selected = field
306+ break
307+ if not selected :
308+ print ("Invalid field selection." )
309+ return None
310+
311+ old_name = selected ["name" ]
312+ print (f"Renaming field '{ old_name } '" )
313+ print (
314+ " Warning: This will modify all documents to rename the field. "
315+ "This is an expensive operation for large datasets."
316+ )
317+ new_name = input ("New field name: " ).strip ()
318+ if not new_name :
319+ print ("New field name is required." )
320+ return None
321+ if new_name == old_name :
322+ print ("New name is the same as the old name." )
323+ return None
324+
325+ existing_names = {f ["name" ] for f in fields }
326+ if new_name in existing_names :
327+ print (f"Field '{ new_name } ' already exists." )
328+ return None
329+
330+ return FieldRename (old_name = old_name , new_name = new_name )
331+
332+ def _prompt_rename_index (self , source_schema : Dict [str , Any ]) -> Optional [str ]:
333+ """Prompt user to rename the index."""
334+ current_name = source_schema ["index" ]["name" ]
335+ print (f"Current index name: { current_name } " )
336+ print (
337+ " Note: This only changes the index name. "
338+ "Documents and keys are unchanged."
339+ )
340+ new_name = input ("New index name: " ).strip ()
341+ if not new_name :
342+ print ("New index name is required." )
343+ return None
344+ if new_name == current_name :
345+ print ("New name is the same as the current name." )
346+ return None
347+ return new_name
348+
349+ def _prompt_change_prefix (self , source_schema : Dict [str , Any ]) -> Optional [str ]:
350+ """Prompt user to change the key prefix."""
351+ current_prefix = source_schema ["index" ]["prefix" ]
352+ print (f"Current prefix: { current_prefix } " )
353+ print (
354+ " Warning: This will RENAME all keys from the old prefix to the new prefix. "
355+ "This is an expensive operation for large datasets."
356+ )
357+ new_prefix = input ("New prefix: " ).strip ()
358+ if not new_prefix :
359+ print ("New prefix is required." )
360+ return None
361+ if new_prefix == current_prefix :
362+ print ("New prefix is the same as the current prefix." )
363+ return None
364+ return new_prefix
245365
246366 def _prompt_common_attrs (
247367 self , field_type : str , allow_blank : bool = False
@@ -262,6 +382,14 @@ def _prompt_common_attrs(
262382 if index_missing is not None :
263383 attrs ["index_missing" ] = index_missing
264384
385+ # Index empty - index documents where field value is empty string
386+ print (
387+ " Index empty: enables isempty() queries for documents with empty string values"
388+ )
389+ index_empty = self ._prompt_bool ("Index empty" , allow_blank = allow_blank )
390+ if index_empty is not None :
391+ attrs ["index_empty" ] = index_empty
392+
265393 # Type-specific attributes
266394 if field_type == "text" :
267395 self ._prompt_text_attrs (attrs , allow_blank )
@@ -302,11 +430,21 @@ def _prompt_text_attrs(self, attrs: Dict[str, Any], allow_blank: bool) -> None:
302430 except ValueError :
303431 print ("Invalid weight value." )
304432
305- # Index empty (requires Redis Search 2.10+)
306- print (" Index empty: enables searching for empty string values" )
307- index_empty = self ._prompt_bool ("Index empty" , allow_blank = allow_blank )
308- if index_empty is not None :
309- attrs ["index_empty" ] = index_empty
433+ # Phonetic matcher
434+ print (
435+ " Phonetic matcher: enables phonetic matching (e.g., 'dm:en' for Metaphone)"
436+ )
437+ phonetic = input ("Phonetic matcher [leave blank for none]: " ).strip ()
438+ if phonetic :
439+ attrs ["phonetic_matcher" ] = phonetic
440+
441+ # Withsuffixtrie
442+ print (" Suffix trie: enables suffix/contains queries (*suffix, *contains*)" )
443+ withsuffixtrie = self ._prompt_bool (
444+ "Enable suffix trie" , allow_blank = allow_blank
445+ )
446+ if withsuffixtrie is not None :
447+ attrs ["withsuffixtrie" ] = withsuffixtrie
310448
311449 # UNF (only if sortable)
312450 if attrs .get ("sortable" ):
@@ -329,11 +467,13 @@ def _prompt_tag_attrs(self, attrs: Dict[str, Any], allow_blank: bool) -> None:
329467 if case_sensitive is not None :
330468 attrs ["case_sensitive" ] = case_sensitive
331469
332- # Index empty (requires Redis Search 2.10+)
333- print (" Index empty: enables searching for empty tag values" )
334- index_empty = self ._prompt_bool ("Index empty" , allow_blank = allow_blank )
335- if index_empty is not None :
336- attrs ["index_empty" ] = index_empty
470+ # Withsuffixtrie
471+ print (" Suffix trie: enables suffix/contains queries (*suffix, *contains*)" )
472+ withsuffixtrie = self ._prompt_bool (
473+ "Enable suffix trie" , allow_blank = allow_blank
474+ )
475+ if withsuffixtrie is not None :
476+ attrs ["withsuffixtrie" ] = withsuffixtrie
337477
338478 def _prompt_numeric_attrs (
339479 self , attrs : Dict [str , Any ], allow_blank : bool , sortable : Optional [bool ]
@@ -439,6 +579,33 @@ def _prompt_vector_attrs(self, field: Dict[str, Any]) -> Dict[str, Any]:
439579 if ef_input and ef_input .isdigit ():
440580 attrs ["ef_construction" ] = int (ef_input )
441581
582+ print (
583+ " EF_RUNTIME: query-time search depth (higher=better recall, slower queries)"
584+ )
585+ ef_runtime_input = input (
586+ f"EF_RUNTIME [current: { current .get ('ef_runtime' , 10 )} ]: "
587+ ).strip ()
588+ if ef_runtime_input and ef_runtime_input .isdigit ():
589+ ef_runtime_val = int (ef_runtime_input )
590+ if ef_runtime_val > 0 :
591+ attrs ["ef_runtime" ] = ef_runtime_val
592+
593+ print (
594+ " EPSILON: relative factor for range queries (0.0-1.0, lower=more accurate)"
595+ )
596+ epsilon_input = input (
597+ f"EPSILON [current: { current .get ('epsilon' , 0.01 )} ]: "
598+ ).strip ()
599+ if epsilon_input :
600+ try :
601+ epsilon_val = float (epsilon_input )
602+ if 0.0 <= epsilon_val <= 1.0 :
603+ attrs ["epsilon" ] = epsilon_val
604+ else :
605+ print (" Epsilon must be between 0.0 and 1.0, ignoring." )
606+ except ValueError :
607+ print (" Invalid epsilon value, ignoring." )
608+
442609 elif effective_algo == "SVS-VAMANA" :
443610 print (
444611 " GRAPH_MAX_DEGREE: max edges per node (higher=better recall, more memory)"
0 commit comments