PageSourceSearch

https://www.paxlovid.com/lib/blocks/find-a-doctor/find-doctor-search-example.mjs

js paxlovid.com collected 2026-10-03 23:29:58 UTC 12,302 bytes, 373 lines download raw bytes

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.