1/** 2 * ElasticSearch Doctor Data Structure 3 * 4 * The data structure represents a doctor's profile in ElasticSearch with the following main components: 5 * 6 * 1. Basic Information: 7 * - id: Unique identifier 8 * - prefix: Title prefix (e.g., "Dr.") 9 * - first_name, last_name: Doctor's name 10 * - suffix: Title suffix (e.g., "MD") 11 * - title: Professional title 12 * - accepts_new_patients: Boolean (stored as 1/0) 13 * - avg_rating: Numerical rating 14 * - bio: Full biography 15 * - brief_bio: Short biography 16 * 17 * 2. Awards (Nested Array): 18 * - award_name: Name of the award 19 * - awarding_institution: Institution giving the award 20 * - received_years: Year received 21 * - id, provider_id, rel_id: Reference IDs 22 * 23 * 3. Certifications (Nested Array): 24 * - certification_specialty_name: Primary specialty 25 * - certification_sub_specialty_name: Sub-specialty 26 * - certifying_body_name: Certifying institution 27 * - year_achieved: Year certified 28 * - Various IDs for references 29 * 30 * 4. Education (Nested Array): 31 * - education_type_name: Type of education 32 * - education_name: Institution name 33 * - graduation_year: Year graduated 34 * - area_of_focus: Specialization 35 * - Various IDs and reference fields 36 * 37 * 5. Practice Information (Nested Array): 38 * - name: Practice name 39 * - address: Street address 40 * - city_name, state_name: Location 41 * - zipcode: ZIP/Postal code 42 * - phone_number: Contact number 43 * - email: Contact email 44 * - website: Practice website 45 * - latitude, longitude: Coordinates 46 * - out_of_network: Insurance network status (Boolean) 47 * 48 * 6. Specialties (Nested Array): 49 * - specialty_name: Primary specialty 50 * - sub_specialty_name: Sub-specialty 51 * - primary_specialty: Boolean flag (1/0) 52 * 53 * 7. Virtual Visits (Nested Array): 54 * - enabled: Availability flag (1/0) 55 * - accept_new_patients: New patient flag (1/0) 56 * - name: Platform name 57 * 58 * 8. Additional Details (Nested Object): 59 * - Various administrative and personal details 60 * - Insurance information 61 * - Licensing and credentials 62 * - Contact preferences 63 * 64 * The data structure uses nested arrays extensively for one-to-many relationships, 65 * and all Boolean values are stored as 1 (true) or 0 (false). 66 * IDs are used throughout for referential integrity. 67 */ 68 69// Constants for common error messages 70const ERROR_MESSAGES = { 71 INVALID_SOURCE: 'Invalid source data provided', 72 MISSING_NAME: 'First name and last name are required', 73 INVALID_ES_FORMAT: 'Invalid ElasticSearch data format', 74 GENERAL_FORMAT_ERROR: (msg) => `Error formatting doctor data: ${msg}`, 75}; 76 77// Constants for field names to avoid typos and enable easier refactoring 78const FIELDS = { 79 FIRST_NAME: 'first_name', 80 LAST_NAME: 'last_name', 81 SPECIALTY_NAME: 'specialty_name', 82 SUB_SPECIALTY_NAME: 'sub_specialty_name', 83}; 84 85/** 86 * Parses and displays doctor search data from ElasticSearch 87 * @param {Object} source - Raw doctor data from ElasticSearch source field 88 * @returns {Object} Formatted doctor information 89 * @throws {Error} If source is invalid or required fields are missing 90 */ 91export function formatDoctorData(source) { 92 if (!source || typeof source !== 'object') { 93 throw new Error(ERROR_MESSAGES.INVALID_SOURCE); 94 } 95 96 // Helper function to safely map array data with error handling 97 const safeMap = (arr, mapFn) => { 98 try { 99 return Array.isArray(arr) ? arr.map(mapFn) : []; 100 } catch (error) { 101 console.error('Error mapping array:', error); 102 return []; 103 } 104 }; 105 106 // Helper function to format name with validation 107 const formatName = ({ prefix = '', firstName = '', lastName = '', suffix = '', title = '' } = {}) => { 108 if (!firstName || !lastName) { 109 throw new Error(ERROR_MESSAGES.MISSING_NAME); 110 } 111 112 const nameParts = [prefix, firstName, lastName, suffix].filter(Boolean); 113 return { 114 prefix, 115 firstName, 116 lastName, 117 suffix, 118 title, 119 fullName: nameParts.join(' '), 120 }; 121 }; 122 123 // Helper function to format address with validation 124 const formatAddress = (practice) => { 125 if (!practice) return ''; 126 const parts = [practice.address, practice.city_name, practice.state_name, practice.zipcode].filter(
126Boolean); 127 return parts.join(', '); 128 }; 129 130 // Helper function to safely convert to boolean with type checking 131 const toBoolean = (value) => Boolean(Number(value)); 132 133 // Helper function to safely convert to number 134 const toNumber = (value) => { 135 const num = Number(value); 136 return Number.isNaN(num) ? null : num; 137 }; 138 139 try { 140 return { 141 // Basic info 142 id: source.provider_id ?? source.id, 143 name: formatName({ 144 prefix: source.prefix, 145 firstName: source[FIELDS.FIRST_NAME], 146 lastName: source[FIELDS.LAST_NAME], 147 suffix: source.credential_name || source.suffix, 148 title: source.specialty_name || source.title, 149 }), 150 151 // Education with improved validation 152 education: safeMap(source.education, (edu) => ({ 153 type: edu.education_type_name || '', 154 institution: edu.education_name || '', 155 graduationYear: toNumber(edu.graduation_year), 156 focus: edu.area_of_focus || '', 157 })), 158 159 // Practice details with improved location handling 160 practices: safeMap(source.practice, (practice) => ({ 161 name: practice.name || '', 162 address: practice.address || '', 163 city: practice.city_name || '', 164 state: practice.state_abbreviation || practice.state_name || '', 165 zip: practice.zipcode || '', 166 phone: practice.phone_number || '', 167 website: practice.website || '', 168 location: { 169 lat: toNumber(practice.location?.lat ?? practice.latitude), 170 lng: toNumber(practice.location?.lon ?? practice.longitude), 171 }, 172 formattedAddress: formatAddress(practice), 173 })), 174 175 // Specialties with improved field access 176 specialties: safeMap(source.specialty, (spec) => ({ 177 name: spec[FIELDS.SPECIALTY_NAME] || '', 178 subSpecialty: spec[FIELDS.SUB_SPECIALTY_NAME] || '', 179 isPrimary: toBoolean(spec.primary_specialty), 180 })), 181 }; 182 } catch (error) { 183 throw new Error(ERROR_MESSAGES.GENERAL_FORMAT_ERROR(error.message)); 184 } 185} 186 187/** 188 * Class to handle doctor search functionality using ElasticSearch data 189 */ 190export default class DoctorSearch { 191 /** 192 * @param {Object} data - ElasticSearch response data 193 * @throws {Error} If data is invalid 194 */ 195 constructor(data) { 196 if (!data?.hits?.hits || !Array.isArray(data.hits.hits)) { 197 throw new Error(ERROR_MESSAGES.INVALID_ES_FORMAT); 198 } 199 this.data = data; 200 this.hits = data.hits.hits; 201 } 202 203 /** 204 * Extract source data from ElasticSearch hit 205 * @param {Object} hit - ElasticSearch hit object 206 * @returns {Object} Source data 207 */ 208 static extractSource(hit) { 209 if (!hit || typeof hit !== 'object') { 210 return {}; 211 } 212 // eslint-disable-next-line no-underscore-dangle 213 return hit?.source || hit?._source || {}; 214 } 215 216 /** 217 * Helper method to filter and format results with improved error handling 218 * @private 219 * @param {Function} filterFn - Filter function to apply 220 * @returns {Array} Formatted results 221 */ 222 filterAndFormat(filterFn) { 223 try { 224 return this.hits 225 .filter((hit) => { 226 try { 227 return filterFn(hit); 228 } catch (error) { 229 console.error('Error in filter function:', error); 230 return false; 231 } 232 }) 233 .map((hit) => formatDoctorData(DoctorSearch.extractSource(hit))); 234 } catch (error) { 235 console.error('Error filtering and formatting results:', error); 236 return []; 237 } 238 } 239 240 /** 241 * Search doctors by name with improved string handling 242 * @param {string} query - Name to search for 243 * @returns {Array} Matching doctors 244 */ 245 searchByName(query) { 246 if (!query?.trim()) return []; 247 248 const searchTerm = query.toLowerCase().trim(); 249 return this.filterAndFormat((hit) => { 250 try { 251 const source = DoctorSearch.extractSource(hit); 252 const fullName = `${source[FIELDS.FIRST_NAME] || ''} ${source[FIELDS.LAST_NAME] || ''}`.toLowerCase(); 253 return fullName.includes(searchTerm); 254 } catch (error) { 255 console.error('Error searching by name:', error); 256 return false;
257 } 258 }); 259 } 260 261 /** 262 * Search doctors by specialty with improved matching 263 * @param {string} specialty - Specialty to search for 264 * @returns {Array} Matching doctors 265 */ 266 searchBySpecialty(specialty) { 267 if (!specialty?.trim()) return []; 268 269 const searchTerm = specialty.toLowerCase().trim(); 270 271 return this.filterAndFormat((hit) => { 272 try { 273 const source = DoctorSearch.extractSource(hit); 274 return ( 275 source.specialty?.some((spec) => { 276 const specialtyName = (spec[FIELDS.SPECIALTY_NAME] || '').toLowerCase(); 277 const subSpecialtyName = (spec[FIELDS.SUB_SPECIALTY_NAME] || '').toLowerCase(); 278 return specialtyName.includes(searchTerm) || subSpecialtyName.includes(searchTerm); 279 }) || false 280 ); 281 } catch (error) { 282 console.error('Error searching by specialty:', error); 283 return false; 284 } 285 }); 286 } 287 288 /** 289 * Search doctors by location with improved validation 290 * @param {string} zipCode - ZIP code to search for 291 * @returns {Array} Matching doctors 292 */ 293 searchByLocation(zipCode) { 294 if (!zipCode?.trim()) return []; 295 296 const cleanZipCode = zipCode.trim(); 297 return this.filterAndFormat((hit) => { 298 try { 299 const source = DoctorSearch.extractSource(hit); 300 return source.practice?.some((practice) => practice.zipcode === cleanZipCode) || false; 301 } catch (error) { 302 console.error('Error searching by location:', error); 303 return false; 304 } 305 }); 306 } 307 308 /** 309 * Search doctors using multiple criteria with improved filtering 310 * @param {Object} params - Search parameters 311 * @param {string} [params.name] - Doctor name 312 * @param {string} [params.specialty] - Doctor specialty 313 * @param {string} [params.location] - ZIP code 314 * @returns {Array} Matching doctors 315 */ 316 search({ name, specialty, location } = {}) { 317 try { 318 let results = this.hits; 319 320 if (name?.trim()) { 321 const searchName = name.toLowerCase().trim(); 322 results = results.filter((hit) => { 323 const source = DoctorSearch.extractSource(hit); 324 const fullName = `${source[FIELDS.FIRST_NAME] || ''} ${source[FIELDS.LAST_NAME] || ''}`.toLowerCase(); 325 return fullName.includes(searchName); 326 }); 327 } 328 329 if (specialty?.trim()) { 330 const searchSpecialty = specialty.toLowerCase().trim(); 331 results = results.filter((hit) => { 332 const source = DoctorSearch.extractSource(hit); 333 return ( 334 source.specialty?.some((spec) => { 335 const specialtyName = (spec[FIELDS.SPECIALTY_NAME] || '').toLowerCase(); 336 const subSpecialtyName = (spec[FIELDS.SUB_SPECIALTY_NAME] || '').toLowerCase(); 337 return specialtyName.includes(searchSpecialty) || subSpecialtyName.includes(searchSpecialty); 338 }) || false 339 ); 340 }); 341 } 342 343 if (location?.trim()) { 344 const cleanLocation = location.trim(); 345 results = results.filter((hit) => { 346 const source = DoctorSearch.extractSource(hit); 347 return source.practice?.some((practice) => practice.zipcode === cleanLocation) || false; 348 }); 349 } 350 351 const doctors = results.map((hit) => formatDoctorData(DoctorSearch.extractSource(hit))); 352 return doctors; 353 } catch (error) { 354 console.error('Error performing combined search:', error); 355 return []; 356 } 357 } 358 359 /** 360 * Search for a doctor by ID 361 * @param {string} id - Doctor ID to search for 362 * @returns {Object|null} Doctor object if found, null otherwise 363 */ 364 searchById(id) { 365 try { 366 const doctor = this.data.hits.hits.find((hit) => hit._source.id.toString() === id.toString()); 367 return doctor ? doctor._source : null; 368 } catch (error) { 369 console.error('Error searching doctor by ID:', error); 370 return null; 371 } 372 } 373}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.