1616from basic_memory .repository .embedding_provider_factory import create_embedding_provider
1717from basic_memory .repository .search_index_row import SearchIndexRow
1818from basic_memory .repository .search_repository_base import SearchRepositoryBase
19- from basic_memory .repository .metadata_filters import (
20- parse_metadata_filters ,
21- build_postgres_json_path ,
22- )
19+ from basic_memory .repository .metadata_filters import parse_metadata_filters
2320from basic_memory .repository .semantic_errors import SemanticDependenciesMissingError
2421from basic_memory .schemas .search import SearchItemType , SearchRetrievalMode
2522
@@ -677,19 +674,23 @@ async def search(
677674 else :
678675 conditions .append ("search_index.permalink = :permalink" )
679676
680- # Handle search item type filter
677+ # Handle search item type filter (parameterized for defense-in-depth)
681678 if search_item_types :
682- type_list = ", " .join (f"'{ t .value } '" for t in search_item_types )
683- conditions .append (f"search_index.type IN ({ type_list } )" )
684-
685- # Handle note type filter using JSONB containment (frontmatter type field)
679+ type_placeholders = []
680+ for idx , t in enumerate (search_item_types ):
681+ param_name = f"search_type_{ idx } "
682+ params [param_name ] = t .value
683+ type_placeholders .append (f":{ param_name } " )
684+ conditions .append (f"search_index.type IN ({ ', ' .join (type_placeholders )} )" )
685+
686+ # Handle note type filter using JSONB containment (parameterized)
686687 if note_types :
687- # Use JSONB @> operator for efficient containment queries
688688 type_conditions = []
689- for note_type in note_types :
690- # Create JSONB containment condition for each note type
689+ for idx , note_type in enumerate (note_types ):
690+ param_name = f"note_type_{ idx } "
691+ params [param_name ] = json .dumps ({"note_type" : note_type })
691692 type_conditions .append (
692- f' search_index.metadata @> \' {{"note_type": " { note_type } "}} \' '
693+ f" search_index.metadata @> CAST(: { param_name } AS jsonb)"
693694 )
694695 conditions .append (f"({ ' OR ' .join (type_conditions )} )" )
695696
@@ -701,15 +702,23 @@ async def search(
701702 order_by_clause = ", search_index.updated_at DESC"
702703
703704 # Handle structured metadata filters (frontmatter)
705+ # Uses jsonb_extract_path_text() / jsonb_extract_path() with parameterized
706+ # path parts instead of #>> / #> with interpolated paths.
704707 if metadata_filters :
705708 parsed_filters = parse_metadata_filters (metadata_filters )
706709 from_clause = "search_index JOIN entity ON search_index.entity_id = entity.id"
707710 metadata_expr = "entity.entity_metadata::jsonb"
708711
709712 for idx , filt in enumerate (parsed_filters ):
710- path = build_postgres_json_path (filt .path_parts )
711- text_expr = f"({ metadata_expr } #>> '{ path } ')"
712- json_expr = f"({ metadata_expr } #> '{ path } ')"
713+ # Parameterize each JSON path part individually
714+ path_param_names = []
715+ for j , part in enumerate (filt .path_parts ):
716+ path_param = f"meta_path_{ idx } _{ j } "
717+ params [path_param ] = part
718+ path_param_names .append (f":{ path_param } " )
719+ path_args = ", " .join (path_param_names )
720+ text_expr = f"jsonb_extract_path_text({ metadata_expr } , { path_args } )"
721+ json_expr = f"jsonb_extract_path({ metadata_expr } , { path_args } )"
713722
714723 if filt .op == "eq" :
715724 value_param = f"meta_val_{ idx } "
@@ -727,14 +736,12 @@ async def search(
727736 continue
728737
729738 if filt .op == "contains" :
730- import json as _json
731-
732739 base_param = f"meta_val_{ idx } "
733740 tag_conditions = []
734741 # Require all values to be present
735742 for j , val in enumerate (filt .value ):
736743 tag_param = f"{ base_param } _{ j } "
737- params [tag_param ] = _json .dumps ([val ])
744+ params [tag_param ] = json .dumps ([val ])
738745 like_param = f"{ base_param } _{ j } _like"
739746 params [like_param ] = f'%"{ val } "%'
740747 like_param_single = f"{ base_param } _{ j } _like_single"
@@ -749,7 +756,7 @@ async def search(
749756
750757 if filt .op in {"gt" , "gte" , "lt" , "lte" , "between" }:
751758 compare_expr = (
752- f"( { metadata_expr } #>> ' { path } ') ::double precision"
759+ f"{ text_expr } ::double precision"
753760 if filt .comparison == "numeric"
754761 else text_expr
755762 )
0 commit comments