11import json
22import os
3+ from abc import ABC , abstractmethod
4+ from typing import Any , Dict , Union
35
6+ import numpy as np
47import pandas as pd
58from tqdm import tqdm
69from weaviate .util import generate_uuid5
710
811from datastew .embedding import Vectorizer
9- from datastew .repository import WeaviateRepository
12+ from datastew .repository import PostgreSQLRepository , SQLLiteRepository , WeaviateRepository
13+ from datastew .repository .base import BaseRepository
14+ from datastew .repository .model import Concept , Mapping , Terminology
1015from datastew .repository .weaviate_schema import (
1116 concept_schema ,
1217 mapping_schema_user_vectors ,
1318 terminology_schema ,
1419)
1520
1621
17- class WeaviateJsonlConverter (object ):
18- """
19- Converts data to our JSONL format for Weaviate schema.
20- """
22+ class BaseJsonlConverter (ABC ):
23+ def __init__ (self , dest_dir : str , buffer_size : int = 1000 ):
24+ """Initialize the converter.
2125
22- def __init__ (
23- self ,
24- dest_dir : str ,
25- terminology_schema : dict = terminology_schema .schema ,
26- concept_schema : dict = concept_schema .schema ,
27- mapping_schema : dict = mapping_schema_user_vectors .schema ,
28- buffer_size : int = 1000 ,
29- ):
26+ :param dest_dir: Destination directory for the exported JSONL files.
27+ :param buffer_size: Number of records to buffer before writing to disk, defaults to 1000
28+ """
3029 self .dest_dir = dest_dir
31- self .terminology_schema = terminology_schema
32- self .concept_schema = concept_schema
33- self .mapping_schema = mapping_schema
3430 self ._buffer = []
3531 self ._buffer_size = buffer_size
3632 self ._ensure_directories_exist ()
3733
3834 def _ensure_directories_exist (self ):
39- """
40- Ensures the output directory exists.
41-
42- :return: None
43- """
35+ """Ensures the output directory exists."""
4436 os .makedirs (self .dest_dir , exist_ok = True )
4537
4638 def _get_file_path (self , collection : str ) -> str :
@@ -79,33 +71,187 @@ def _flush_to_file(self, file_path: str):
7971
8072 with open (file_path , "a" , encoding = "utf-8" ) as file :
8173 for entry in self ._buffer :
74+ if isinstance (entry .get ("embedding" ), np .ndarray ):
75+ entry ["embedding" ] = entry ["embedding" ].tolist ()
8276 file .write (json .dumps (entry ) + "\n " )
8377
8478 self ._buffer .clear ()
8579
86- def from_repository (self , repository : WeaviateRepository ) -> None :
80+ @abstractmethod
81+ def from_repository (self , repository : BaseRepository ):
82+ pass
83+
84+ @abstractmethod
85+ def from_ohdsi (self , src : str , vectorizer : Vectorizer = Vectorizer (), include_vectors : bool = True ):
86+ pass
87+
88+ @abstractmethod
89+ def _object_to_dict (self , obj : Any ) -> Dict [str , Any ]:
90+ pass
91+
92+
93+ class SQLJsonlConverter (BaseJsonlConverter ):
94+ def __init__ (self , dest_dir : str , buffer_size : int = 1000 ):
95+ super ().__init__ (dest_dir = dest_dir , buffer_size = buffer_size )
96+
97+ def from_repository (self , repository : Union [PostgreSQLRepository , SQLLiteRepository ]):
98+ """Export all records from a PostgreSQLRepository to JSONL files
99+
100+ :param repository: Active database repository instace.
101+ """
102+ session = repository .session
103+
104+ # Export Terminologies
105+ terminology_file_path = self ._get_file_path ("terminology" )
106+ for t in tqdm (session .query (Terminology ).all (), desc = "Exporting Terminologies" ):
107+ terminology = self ._object_to_dict (t )
108+ self ._write_to_jsonl (terminology_file_path , terminology )
109+ self ._flush_to_file (terminology_file_path )
110+
111+ # Export Concepts
112+ concept_file_path = self ._get_file_path ("concept" )
113+ for c in tqdm (session .query (Concept ).all (), desc = "Exporting Concepts" ):
114+ concept = self ._object_to_dict (c )
115+ self ._write_to_jsonl (concept_file_path , concept )
116+ self ._flush_to_file (concept_file_path )
117+
118+ # Export Mappings
119+ mapping_file_path = self ._get_file_path ("mapping" )
120+ for m in tqdm (session .query (Mapping ).all (), desc = "Exporting Mappings" ):
121+ mapping = self ._object_to_dict (m )
122+ self ._write_to_jsonl (mapping_file_path , mapping )
123+ self ._flush_to_file (mapping_file_path )
124+
125+ def from_ohdsi (self , src : str , vectorizer : Vectorizer = Vectorizer (), include_vectors : bool = True ):
126+ """
127+ Converts data from OHDSI to SQL-compatible JSONL format.
128+
129+ :param src: Path to the OHDSI CONCEPT.csv file.
130+ :param vectorizer: Vectorizer to use for text embeddings.
131+ :param include_vectors: Whether to include vector data in mappings.
132+ """
133+ if not os .path .exists (src ):
134+ raise FileNotFoundError (f"OHDSI concept file '{ src } ' does not exist or is not a file." )
135+
136+ terminology_file_path = self ._get_file_path ("terminology" )
137+ concept_file_path = self ._get_file_path ("concept" )
138+ mapping_file_path = self ._get_file_path ("mapping" )
139+
140+ # Write single OHDSI terminology entry
141+ self ._write_to_jsonl (terminology_file_path , {"id" : "OHDSI" , "name" : "OHDSI" })
142+ self ._flush_to_file (terminology_file_path )
143+
144+ for chunk in tqdm (
145+ pd .read_csv (
146+ src ,
147+ delimiter = "\t " ,
148+ usecols = ["concept_name" , "concept_id" ],
149+ chunksize = 10000 ,
150+ dtype = {"concept_id" : str , "concept_name" : str },
151+ ),
152+ desc = "Processing OHDSI concepts" ,
153+ ):
154+
155+ concepts = []
156+ mappings = []
157+
158+ concept_names = chunk ["concept_name" ].astype (str ).tolist ()
159+ concept_ids = chunk ["concept_id" ].astype (str ).tolist ()
160+
161+ if include_vectors :
162+ embeddings = vectorizer .get_embeddings (concept_names )
163+
164+ for i in range (len (concept_names )):
165+ concept_identifier = f"OHDSI:{ concept_ids [i ]} "
166+ label = concept_names [i ]
167+
168+ # Concept JSON
169+ concepts .append (
170+ {
171+ "concept_identifier" : concept_identifier ,
172+ "pref_label" : label ,
173+ "terminology_id" : "OHDSI" ,
174+ }
175+ )
176+
177+ # Mapping JSON
178+ mapping = {
179+ "concept_identifier" : concept_identifier ,
180+ "text" : label ,
181+ }
182+ if include_vectors :
183+ mapping ["sentence_embedder" ] = vectorizer .model_name
184+ mapping ["embedding" ] = embeddings [i ]
185+
186+ mappings .append (mapping )
187+
188+ # Write results in batch
189+ for concept_data in concepts :
190+ self ._write_to_jsonl (concept_file_path , concept_data )
191+ self ._flush_to_file (concept_file_path )
192+ for mapping_data in mappings :
193+ self ._write_to_jsonl (mapping_file_path , mapping_data )
194+ self ._flush_to_file (mapping_file_path )
195+
196+ def _object_to_dict (self , obj : Union [Terminology , Concept , Mapping ]) -> Dict [str , Any ]:
197+ if isinstance (obj , Terminology ):
198+ return {
199+ "id" : obj .id ,
200+ "name" : obj .name ,
201+ }
202+ elif isinstance (obj , Concept ):
203+ return {
204+ "concept_identifier" : obj .concept_identifier ,
205+ "pref_label" : obj .pref_label ,
206+ "terminology_id" : obj .terminology .id ,
207+ }
208+ elif isinstance (obj , Mapping ):
209+ return {
210+ "concept_identifier" : obj .concept .concept_identifier ,
211+ "text" : obj .text ,
212+ "embedding" : obj .embedding ,
213+ "sentence_embedder" : obj .sentence_embedder ,
214+ }
215+ else :
216+ raise TypeError (f"Unsupported object type: { type (obj )} " )
217+
218+
219+ class WeaviateJsonlConverter (BaseJsonlConverter ):
220+ def __init__ (
221+ self ,
222+ dest_dir : str ,
223+ terminology_schema : dict = terminology_schema .schema ,
224+ concept_schema : dict = concept_schema .schema ,
225+ mapping_schema : dict = mapping_schema_user_vectors .schema ,
226+ buffer_size : int = 1000 ,
227+ ):
228+ super ().__init__ (dest_dir = dest_dir , buffer_size = buffer_size )
229+ self .terminology_schema = terminology_schema
230+ self .concept_schema = concept_schema
231+ self .mapping_schema = mapping_schema
232+
233+ def from_repository (self , repository : WeaviateRepository ):
87234 """
88235 Converts data from a WeaviateRepository to our JSONL format.
89236
90237 :param repository: WeaviateRepository
91- :return: None
92238 """
93239 # Process terminology first
94240 terminology_file_path = self ._get_file_path ("terminology" )
95241 for terminology in repository .get_iterator (self .terminology_schema ["class" ]):
96- self ._write_to_jsonl (terminology_file_path , self ._weaviate_object_to_dict (terminology ))
242+ self ._write_to_jsonl (terminology_file_path , self ._object_to_dict (terminology ))
97243 self ._flush_to_file (terminology_file_path )
98244
99245 # Process concept next
100246 concept_file_path = self ._get_file_path ("concept" )
101247 for concept in repository .get_iterator (self .concept_schema ["class" ]):
102- self ._write_to_jsonl (concept_file_path , self ._weaviate_object_to_dict (concept ))
248+ self ._write_to_jsonl (concept_file_path , self ._object_to_dict (concept ))
103249 self ._flush_to_file (concept_file_path )
104250
105251 # Process mapping last
106252 mapping_file_path = self ._get_file_path ("mapping" )
107253 for mapping in repository .get_iterator (self .mapping_schema ["class" ]):
108- self ._write_to_jsonl (mapping_file_path , self ._weaviate_object_to_dict (mapping ))
254+ self ._write_to_jsonl (mapping_file_path , self ._object_to_dict (mapping ))
109255 self ._flush_to_file (mapping_file_path )
110256
111257 def from_ohdsi (self , src : str , vectorizer : Vectorizer = Vectorizer (), include_vectors : bool = True ):
@@ -128,7 +274,7 @@ def from_ohdsi(self, src: str, vectorizer: Vectorizer = Vectorizer(), include_ve
128274 terminology_properties = {"name" : "OHDSI" }
129275 terminology_id = generate_uuid5 (terminology_properties )
130276 ohdsi_terminology = {
131- "class" : self . terminology_schema [ "class" ] ,
277+ "class" : "Terminology" ,
132278 "id" : terminology_id ,
133279 "properties" : terminology_properties ,
134280 }
@@ -206,22 +352,25 @@ def from_ohdsi(self, src: str, vectorizer: Vectorizer = Vectorizer(), include_ve
206352 self ._write_to_jsonl (mapping_file_path , mapping_data )
207353 self ._flush_to_file (mapping_file_path )
208354
209- @ staticmethod
210- def _weaviate_object_to_dict ( weaviate_object ):
355+ def _object_to_dict ( self , obj ) -> Dict [ str , Any ]:
356+ """Conver a Weaviate object to a schema-compliant JSON dictionary.
211357
212- if weaviate_object .references is not None :
358+ :param obj: Weaviate object instance.
359+ :return: Formatted dictionary containing class, id, properties, vector, and references.
360+ """
361+ if obj .references is not None :
213362 # FIXME: This is a hack to get the UUID of the referenced object. Replace as soon as weaviate devs offer an
214363 # actual solution for this.
215- vals = [value .objects for _ , value in weaviate_object .references .items ()]
364+ vals = [value .objects for _ , value in obj .references .items ()]
216365 uuid = [str (obj .uuid ) for sublist in vals for obj in sublist ][0 ]
217- references = {key : uuid for key , _ in weaviate_object .references .items ()}
366+ references = {key : uuid for key , _ in obj .references .items ()}
218367 else :
219368 references = {}
220369
221370 return {
222- "class" : weaviate_object .collection ,
223- "id" : str (weaviate_object .uuid ),
224- "properties" : weaviate_object .properties ,
225- "vector" : weaviate_object .vector ,
371+ "class" : obj .collection ,
372+ "id" : str (obj .uuid ),
373+ "properties" : obj .properties ,
374+ "vector" : obj .vector ,
226375 "references" : references ,
227376 }
0 commit comments