@@ -174,7 +174,6 @@ class Embeddings(BaseModel): # type: ignore
174174 __tablename__ = "embeddings"
175175
176176 id = sqla .Column (sqla .String , primary_key = True , default = generate_uuid )
177- embeddings = sqla .Column (sqla .String , nullable = False )
178177 doc_type = sqla .Column (sqla .String , nullable = False )
179178 cre_id = sqla .Column (
180179 sqla .String ,
@@ -191,9 +190,10 @@ class Embeddings(BaseModel): # type: ignore
191190 embeddings_content = sqla .Column (sqla .String , nullable = True , default = None )
192191 embedding_model_id = sqla .Column (sqla .String , nullable = True , default = None )
193192 embedding_dim = sqla .Column (sqla .Integer , nullable = True , default = None )
194- # Postgres: real ``vector(N)`` via Alembic (c7d8e9f0a1b2). SQLite tests map
195- # this as Text storing a pgvector literal for dual-write coverage.
196- embedding_vec = sqla .Column (sqla .Text , nullable = True , default = None )
193+ # Postgres: real ``vector(N)`` via Alembic (c7d8e9f0a1b2) — sole store for
194+ # vectors (legacy CSV ``embeddings`` column is dropped in that migration).
195+ # SQLite/tests: Text holding a pgvector literal ``[1.0,2.0,...]``.
196+ embedding_vec = sqla .Column (sqla .Text , nullable = False )
197197
198198
199199class GapAnalysisResults (BaseModel ):
@@ -2367,16 +2367,27 @@ def health_check(self) -> Dict[str, Any]:
23672367 }
23682368
23692369 def get_embeddings_by_doc_type (self , doc_type : str ) -> Dict [str , List [float ]]:
2370+ from application .database .pgvector_utils import (
2371+ parse_stored_embedding_vec ,
2372+ require_embedding_vec_store ,
2373+ )
2374+
2375+ require_embedding_vec_store (
2376+ self .session .get_bind (), context = "get_embeddings_by_doc_type"
2377+ )
23702378 res = {}
23712379 embeddings = (
23722380 self .session .query (Embeddings ).filter (Embeddings .doc_type == doc_type ).all ()
23732381 )
23742382 if embeddings :
23752383 for entry in embeddings :
2384+ vec = parse_stored_embedding_vec (entry .embedding_vec )
2385+ if not vec :
2386+ continue
23762387 if doc_type == cre_defs .Credoctypes .CRE .value :
2377- res [entry .cre_id ] = [ float ( e ) for e in entry . embeddings . split ( "," )]
2388+ res [entry .cre_id ] = vec
23782389 else :
2379- res [entry .node_id ] = [ float ( e ) for e in entry . embeddings . split ( "," )]
2390+ res [entry .node_id ] = vec
23802391 return res
23812392
23822393 def get_embedding_contents_by_doc_type (self , doc_type : str ) -> Dict [str , str ]:
@@ -2403,6 +2414,14 @@ def get_embedding_contents_by_doc_type(self, doc_type: str) -> Dict[str, str]:
24032414 def get_embeddings_by_doc_type_paginated (
24042415 self , doc_type : str , page : int = 1 , per_page : int = 100
24052416 ) -> Tuple [Dict [str , List [float ]], int , int ]:
2417+ from application .database .pgvector_utils import (
2418+ parse_stored_embedding_vec ,
2419+ require_embedding_vec_store ,
2420+ )
2421+
2422+ require_embedding_vec_store (
2423+ self .session .get_bind (), context = "get_embeddings_by_doc_type_paginated"
2424+ )
24062425 res = {}
24072426 embeddings = (
24082427 self .session .query (Embeddings )
@@ -2412,13 +2431,21 @@ def get_embeddings_by_doc_type_paginated(
24122431 total_pages = embeddings .pages
24132432 if embeddings .items :
24142433 for entry in embeddings .items :
2434+ vec = parse_stored_embedding_vec (entry .embedding_vec )
2435+ if not vec :
2436+ continue
24152437 if doc_type == cre_defs .Credoctypes .CRE .value :
2416- res [entry .cre_id ] = [ float ( e ) for e in entry . embeddings . split ( "," )]
2438+ res [entry .cre_id ] = vec
24172439 else :
2418- res [entry .node_id ] = [ float ( e ) for e in entry . embeddings . split ( "," )]
2440+ res [entry .node_id ] = vec
24192441 return res , total_pages , page
24202442
24212443 def get_embeddings_for_doc (self , doc : cre_defs .Node | cre_defs .CRE ) -> Embeddings :
2444+ from application .database .pgvector_utils import require_embedding_vec_store
2445+
2446+ require_embedding_vec_store (
2447+ self .session .get_bind (), context = "get_embeddings_for_doc"
2448+ )
24222449 if doc .doctype == cre_defs .Credoctypes .CRE :
24232450 obj = self .session .query (CRE ).filter (CRE .external_id == doc .id ).first ()
24242451 return (
@@ -2487,13 +2514,15 @@ def add_embedding(
24872514 f"embedding dimension mismatch for { db_object .id } : "
24882515 f"expected { expected_dim } , got { len (embeddings )} "
24892516 )
2490- existing = self .get_embedding (db_object .id )
2491- embeddings_str = "," .join ([str (e ) for e in embeddings ])
2492- # Dual-write CSV + pgvector literal. On Postgres after migration the
2493- # column is ``vector(N)``; on SQLite tests it is Text. Never LLM
2494- # re-embed here — see application.database.pgvector_utils.
2495- from application .database .pgvector_utils import to_pgvector_literal
2517+ from application .database .pgvector_utils import (
2518+ require_embedding_vec_store ,
2519+ to_pgvector_literal ,
2520+ )
24962521
2522+ require_embedding_vec_store (self .session .get_bind (), context = "add_embedding" )
2523+ existing = self .get_embedding (db_object .id )
2524+ # Sole store is ``embedding_vec`` (pgvector on Postgres; Text literal
2525+ # on SQLite). Never LLM re-embed here — see pgvector_utils.
24972526 embedding_vec_literal = to_pgvector_literal (embeddings )
24982527 resolved_node_url : Optional [str ] = None
24992528 if doctype != cre_defs .Credoctypes .CRE :
@@ -2505,7 +2534,6 @@ def add_embedding(
25052534 emb = None
25062535 if doctype == cre_defs .Credoctypes .CRE :
25072536 emb = Embeddings (
2508- embeddings = embeddings_str ,
25092537 cre_id = db_object .id ,
25102538 doc_type = cre_defs .Credoctypes .CRE .value ,
25112539 embeddings_content = embedding_text ,
@@ -2515,7 +2543,6 @@ def add_embedding(
25152543 )
25162544 else :
25172545 emb = Embeddings (
2518- embeddings = embeddings_str ,
25192546 node_id = db_object .id ,
25202547 doc_type = db_object .ntype ,
25212548 embeddings_content = embedding_text ,
@@ -2529,7 +2556,6 @@ def add_embedding(
25292556 return emb
25302557 else :
25312558 logger .debug (f"knew of embedding for object { db_object .id } ,updating" )
2532- existing [0 ].embeddings = embeddings_str
25332559 existing [0 ].embeddings_content = embedding_text
25342560 existing [0 ].embedding_model_id = embedding_model_id
25352561 existing [0 ].embedding_dim = embedding_dim
@@ -2571,14 +2597,20 @@ def find_most_similar_embedding_id(
25712597 """Top-1 cosine match via pgvector ``<=>`` (Postgres only).
25722598
25732599 Returns ``(object_id, score)`` or ``(None, None)`` below threshold /
2574- when no rows match.
2600+ when no rows match. Refuses SQLite / missing ``embedding_vec`` with
2601+ ``SystemExit`` — callers that need a soft fallback must gate on
2602+ ``can_use_pgvector_similarity()`` first and use sklearn instead.
25752603 """
25762604 from application .database .pgvector_utils import (
2605+ fail_pgvector_unavailable ,
25772606 most_similar_id_sql ,
25782607 to_pgvector_literal ,
25792608 )
25802609 from sqlalchemy import text as sql_text
25812610
2611+ if not self .can_use_pgvector_similarity ():
2612+ fail_pgvector_unavailable (context = "find_most_similar_embedding_id" )
2613+
25822614 sql = most_similar_id_sql (id_column )
25832615 bind = self .session .get_bind ()
25842616 params = {
@@ -2599,6 +2631,8 @@ def find_most_similar_embedding_id(
25992631 if score < similarity_threshold :
26002632 return None , None
26012633 return str (row .object_id ), score
2634+ except SystemExit :
2635+ raise
26022636 except Exception as exc :
26032637 # Match prior sklearn-path resilience: chat/import should degrade
26042638 # to "no match" rather than 500 on a transient driver/query error.
0 commit comments