@@ -303,27 +303,21 @@ def search(
303303 except AttributeError :
304304 df = df .applymap (clean_cols )
305305
306- # Keep synonyms always of type list for consistency
306+ # Keep synonyms always of type list for consistency, and normalize NaN
307+ # inside the list to None so an empty-synonym row is [None] rather than
308+ # [nan]. (SQL LEFT JOIN on external_synonym surfaces missing rows as NaN.)
307309 df ["synonym" ] = [
308- np .sort (syn ).tolist () if isinstance (syn , list ) else np .sort ([syn ]).tolist () for syn in df ["synonym" ].values
310+ [None if pd .isna (item ) else item
311+ for item in np .sort (syn if isinstance (syn , list ) else [syn ]).tolist ()]
312+ for syn in df ["synonym" ].values
309313 ]
310314
311- # Normalize missing values to None across the frame so output is stable:
312- # SQL LEFT JOINs surface unmatched rows as NaN in pandas, which then leaks
313- # into both scalar cells and the synonym lists ([nan] instead of [None]).
314- # Callers (and the JSON path) expect None.
315- def _nan_to_none (value ):
316- if isinstance (value , list ):
317- return [None if pd .isna (item ) else item for item in value ]
318- try :
319- return None if pd .isna (value ) else value
320- except (TypeError , ValueError ):
321- return value
322-
323- try :
324- df = df .map (_nan_to_none )
325- except AttributeError :
326- df = df .applymap (_nan_to_none )
315+ # Normalize scalar NaN -> None across the frame so output is stable.
316+ # `astype(object)` first defeats pandas 2.1+ arrow-backed `str` columns,
317+ # which would otherwise coerce assigned None back to NaN; list cells
318+ # (synonym) are left untouched by .where since they are "not NA".
319+ df = df .astype (object )
320+ df = df .where (df .notna (), None )
327321
328322 # If limit is not None, keep only the first {limit} rows
329323 if limit is not None :
0 commit comments