Skip to content

Commit ca5072e

Browse files
Improve performance of Organization endpoint (#395)
* adding org view and orgname index * renaming primary_name to name for organization * updating model as well * remove print statement * fixing failing e2e * fix merge conflicts
1 parent 6ca1656 commit ca5072e

9 files changed

Lines changed: 144 additions & 97 deletions

File tree

backend/npdfhir/filters/organization_filter_set.py

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django_filters import rest_framework as filters
44

55
from ..mappings import addressUseMapping
6-
from ..models import Organization
6+
from ..models import OrganizationView
77
from ..utils import parse_identifier_query
88

99

@@ -38,7 +38,7 @@ class OrganizationFilterSet(filters.FilterSet):
3838
)
3939

4040
class Meta:
41-
model = Organization
41+
model = OrganizationView
4242
fields = [
4343
"name",
4444
"identifier",
@@ -51,13 +51,8 @@ class Meta:
5151
]
5252

5353
def filter_name(self, queryset, name, value):
54-
return (
55-
queryset.annotate(
56-
search=SearchVector("organizationtoname__name"),
57-
)
58-
.filter(search=SearchQuery(value, search_type="plain"))
59-
.distinct()
60-
)
54+
query = SearchQuery(f"{value.upper()}", search_type="websearch")
55+
return queryset.filter(organization__organizationtoname__search_vector=query)
6156

6257
def filter_identifier(self, queryset, name, value):
6358
from uuid import UUID
@@ -68,12 +63,12 @@ def filter_identifier(self, queryset, name, value):
6863
if system: # specific identifier search requested
6964
if system.upper() == "NPI":
7065
try:
71-
queries = Q(clinicalorganization__npi__npi=int(identifier_id))
66+
queries = Q(organization__clinicalorganization__npi__npi=int(identifier_id))
7267
except (ValueError, TypeError):
7368
pass # TODO: implement validationerror to show users that NPI must be an int
7469
else: # general identifier search requested
7570
try:
76-
queries |= Q(clinicalorganization__npi__npi=int(identifier_id))
71+
queries |= Q(organization__clinicalorganization__npi__npi=int(identifier_id))
7772
except (ValueError, TypeError):
7873
pass
7974

@@ -83,46 +78,52 @@ def filter_identifier(self, queryset, name, value):
8378
except (ValueError, TypeError):
8479
pass
8580

86-
queries |= Q(clinicalorganization__organizationtootherid__other_id=identifier_id)
81+
queries |= Q(
82+
organization__clinicalorganization__organizationtootherid__other_id=identifier_id
83+
)
8784

8885
return queryset.filter(queries).distinct()
8986

9087
def filter_organization_type(self, queryset, name, value):
9188
return queryset.annotate(
9289
search=SearchVector(
93-
"clinicalorganization__organizationtotaxonomy__nucc_code__display_name"
90+
"organization__clinicalorganization__organizationtotaxonomy__nucc_code__display_name"
9491
)
9592
).filter(search=value)
9693

9794
def filter_address(self, queryset, name, value):
9895
return queryset.annotate(
9996
search=SearchVector(
100-
"organizationtoaddress__address__address_us__delivery_line_1",
101-
"organizationtoaddress__address__address_us__delivery_line_2",
102-
"organizationtoaddress__address__address_us__city_name",
103-
"organizationtoaddress__address__address_us__state_code__abbreviation",
104-
"organizationtoaddress__address__address_us__zipcode",
97+
"organization__organizationtoaddress__address__address_us__delivery_line_1",
98+
"organization__organizationtoaddress__address__address_us__delivery_line_2",
99+
"organization__organizationtoaddress__address__address_us__city_name",
100+
"organization__organizationtoaddress__address__address_us__state_code__abbreviation",
101+
"organization__organizationtoaddress__address__address_us__zipcode",
105102
)
106103
).filter(search=SearchQuery(value, search_type="websearch"))
107104

108105
def filter_address_city(self, queryset, name, value):
109106
return queryset.annotate(
110-
search=SearchVector("organizationtoaddress__address__address_us__city_name")
107+
search=SearchVector(
108+
"organization__organizationtoaddress__address__address_us__city_name"
109+
)
111110
).filter(search=value)
112111

113112
def filter_address_state(self, queryset, name, value):
114113
return queryset.annotate(
115114
search=SearchVector(
116-
"organizationtoaddress__address__address_us__state_code__abbreviation"
115+
"organization__organizationtoaddress__address__address_us__state_code__abbreviation"
117116
)
118117
).filter(search=value)
119118

120119
def filter_address_postalcode(self, queryset, name, value):
121-
return queryset.filter(organizationtoaddress__address__address_us__zipcode=value)
120+
return queryset.filter(
121+
organization__organizationtoaddress__address__address_us__zipcode=value
122+
)
122123

123124
def filter_address_use(self, queryset, name, value):
124125
if value in addressUseMapping.keys():
125126
value = addressUseMapping.toNPD(value)
126127
else:
127128
value = -1
128-
return queryset.filter(organizationtoaddress__address_use_id=value)
129+
return queryset.filter(organization__organizationtoaddress__address_use_id=value)

backend/npdfhir/management/commands/seedsystem.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from npdfhir.tests.fixtures.organization import create_organization
1111
from npdfhir.tests.fixtures.practitioner import create_practitioner
1212

13+
from npdfhir.models import OrganizationView
14+
1315

1416
class Command(BaseCommand):
1517
help = "Create test data for end-to-end specs"
@@ -128,4 +130,5 @@ def handle(self, *args, **options):
128130
self.stdout.write(f"created Endpoint: {self.to_json(id=endpoint.id)}")
129131

130132
self.generate_sample_organizations(25)
133+
OrganizationView.refresh_materialized_view()
131134
self.generate_sample_practitioners(25)

backend/npdfhir/models.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from django.db import models
1+
from django.db import connection, models
22
from django.contrib.gis.db import models as geomodels
33
from django.contrib.postgres.search import SearchVectorField
44

@@ -601,11 +601,17 @@ class OrganizationToName(models.Model):
601601
organization = models.ForeignKey(Organization, models.DO_NOTHING)
602602
name = models.CharField(max_length=1000)
603603
is_primary = models.BooleanField(blank=True, null=True)
604+
search_vector = SearchVectorField(blank=True, null=True)
604605

605606
class Meta:
606607
managed = False
607608
db_table = "organization_to_name"
608609

610+
def _do_insert(self, manager, using, fields, update_pk, raw):
611+
# Prevents the model from attempting to insert values into the generated search_vector field
612+
fields = [f for f in fields if f.attname != "search_vector"]
613+
return super()._do_insert(manager, using, fields, update_pk, raw)
614+
609615

610616
class OrganizationToOtherId(models.Model):
611617
pk = models.CompositePrimaryKey("npi", "other_id", "other_id_type_id", "issuer", "state_code")
@@ -644,6 +650,26 @@ class Meta:
644650
db_table = "organization_to_taxonomy"
645651

646652

653+
class OrganizationView(models.Model):
654+
organization = models.OneToOneField(
655+
Organization, models.DO_NOTHING, primary_key=True, db_column="id"
656+
)
657+
authorized_official = models.ForeignKey(Individual, models.DO_NOTHING, blank=True, null=True)
658+
ein = models.ForeignKey(LegalEntity, models.DO_NOTHING, blank=True, null=True)
659+
parent = models.ForeignKey("self", models.DO_NOTHING, blank=True, null=True)
660+
# the sorting field from organization_to_name
661+
name = models.CharField(max_length=1000)
662+
663+
@classmethod
664+
def refresh_materialized_view(cls):
665+
with connection.cursor() as cursor:
666+
cursor.execute(f"REFRESH MATERIALIZED VIEW {cls._meta.db_table};")
667+
668+
class Meta:
669+
managed = False
670+
db_table = "organization_view"
671+
672+
647673
class OtherIdType(models.Model):
648674
value = models.CharField(max_length=50, blank=True, null=True)
649675

backend/npdfhir/serializers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ class Meta:
311311

312312
def to_representation(self, instance):
313313
request = self.context.get("request")
314+
instance = instance.organization
314315
representation = super().to_representation(instance)
315316

316317
organization = FHIROrganization()
@@ -417,7 +418,7 @@ def to_representation(self, instance):
417418
if len(names) > 1:
418419
aliases = names[1:]
419420
if aliases:
420-
organization.alias = [n['name'] for n in aliases]
421+
organization.alias = [n["name"] for n in aliases]
421422

422423
if instance.parent_id is not None:
423424
organization.partOf = genReference(

backend/npdfhir/tests/test_organization.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django.urls import reverse
22
from rest_framework import status
33

4-
from ..models import Organization, OtherIdType
4+
from ..models import Organization, OtherIdType, OrganizationView
55
from .api_test_case import APITestCase
66
from .fixtures.organization import create_legal_entity, create_organization
77
from .fixtures.location import create_location
@@ -90,13 +90,17 @@ def setUpTestData(cls):
9090
cls.org_cumberland = create_organization(name="Cumberland")
9191
cls.orgs.append(cls.org_cumberland)
9292

93+
OrganizationView.refresh_materialized_view()
94+
9395
return super().setUpTestData()
9496

9597
def setUp(self):
9698
super().setUp()
9799
self.org_without_authorized_official = Organization.objects.create(
98-
id="26708690-19d6-499e-b481-cebe05b98f08", authorized_official_id=None
100+
id="26708690-19d6-499e-b481-cebe05b98f08",
101+
authorized_official_id=None,
99102
)
103+
OrganizationView.refresh_materialized_view()
100104

101105
# Basic tests
102106
def test_list_default(self):
@@ -137,7 +141,7 @@ def test_list_in_default_order(self):
137141

138142
def test_list_in_descending_order(self):
139143
url = reverse("fhir-organization-list")
140-
response = self.client.get(url, {"_sort": "-primary_name"})
144+
response = self.client.get(url, {"_sort": "-name"})
141145
assert_fhir_response(self, response)
142146

143147
# Extract names

backend/npdfhir/tests/test_practitioner_role.py

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,7 @@
1313
# extract_resource_ids,
1414
)
1515

16-
from ..models import (
17-
OrganizationToName,
18-
Provider,
19-
Location,
20-
IndividualToName,
21-
Organization
22-
)
16+
from ..models import OrganizationToName, Provider, IndividualToName, Organization, OrganizationView
2317

2418
from .fixtures.location import create_location
2519
from .fixtures.practitioner import (
@@ -155,6 +149,8 @@ def setUpTestData(cls):
155149

156150
cls.roles_with_params.append(pr)
157151

152+
OrganizationView.refresh_materialized_view()
153+
158154
return super().setUpTestData()
159155

160156
# Basic tests
@@ -563,17 +559,18 @@ def test_list_filter_by_address_city(self):
563559
bundle = response.data["results"]
564560

565561
for entry in bundle["entry"]:
566-
location_id = entry["resource"]["location"][0]["reference"].split("/")[-1]
567562
self.assertIn("resource", entry)
568563
location_entry = entry["resource"]
569564

570565
self.assertEqual(location_entry["resourceType"], "PractitionerRole")
571566
self.assertIn("id", location_entry)
572567
self.assertIn("active", location_entry)
573568

574-
location_obj = Location.objects.get(pk=location_id)
575-
576-
self.assertEqual(city_search, location_obj.address.address_us.city_name)
569+
for entry in bundle["entry"]:
570+
self.assertEqual(1, len(entry["resource"]["location"]))
571+
location_url = entry["resource"]["location"][0]["reference"]
572+
returned_location = self.client.get(location_url).data
573+
self.assertEqual(city_search, returned_location["address"]["city"])
577574

578575
def test_list_filter_by_address_state(self):
579576
state_search = "CA"
@@ -585,17 +582,18 @@ def test_list_filter_by_address_state(self):
585582
bundle = response.data["results"]
586583

587584
for entry in bundle["entry"]:
588-
location_id = entry["resource"]["location"][0]["reference"].split("/")[-1]
589585
self.assertIn("resource", entry)
590586
location_entry = entry["resource"]
591587

592588
self.assertEqual(location_entry["resourceType"], "PractitionerRole")
593589
self.assertIn("id", location_entry)
594590
self.assertIn("active", location_entry)
595591

596-
location_obj = Location.objects.get(pk=location_id)
597-
598-
self.assertEqual(state_search, location_obj.address.address_us.state_code.abbreviation)
592+
for entry in bundle["entry"]:
593+
self.assertEqual(1, len(entry["resource"]["location"]))
594+
location_url = entry["resource"]["location"][0]["reference"]
595+
returned_location = self.client.get(location_url).data
596+
self.assertEqual(state_search, returned_location["address"]["state"])
599597

600598
def test_list_filter_by_address_zip(self):
601599
zip_search = "90001"
@@ -607,17 +605,18 @@ def test_list_filter_by_address_zip(self):
607605
bundle = response.data["results"]
608606

609607
for entry in bundle["entry"]:
610-
location_id = entry["resource"]["location"][0]["reference"].split("/")[-1]
611608
self.assertIn("resource", entry)
612609
location_entry = entry["resource"]
613610

614611
self.assertEqual(location_entry["resourceType"], "PractitionerRole")
615612
self.assertIn("id", location_entry)
616613
self.assertIn("active", location_entry)
617614

618-
location_obj = Location.objects.get(pk=location_id)
619-
620-
self.assertEqual(zip_search, location_obj.address.address_us.zipcode)
615+
for entry in bundle["entry"]:
616+
self.assertEqual(1, len(entry["resource"]["location"]))
617+
location_url = entry["resource"]["location"][0]["reference"]
618+
returned_location = self.client.get(location_url).data
619+
self.assertEqual(zip_search, returned_location["address"]["postalCode"])
621620

622621
def test_list_filter_by_address_zip_leading_zero(self):
623622
zip_search = "05555"
@@ -629,39 +628,45 @@ def test_list_filter_by_address_zip_leading_zero(self):
629628
bundle = response.data["results"]
630629

631630
for entry in bundle["entry"]:
632-
location_id = entry["resource"]["location"][0]["reference"].split("/")[-1]
633631
self.assertIn("resource", entry)
634632
location_entry = entry["resource"]
635633

636634
self.assertEqual(location_entry["resourceType"], "PractitionerRole")
637635
self.assertIn("id", location_entry)
638636
self.assertIn("active", location_entry)
639637

640-
location_obj = Location.objects.get(pk=location_id)
638+
for entry in bundle["entry"]:
639+
self.assertEqual(1, len(entry["resource"]["location"]))
640+
location_url = entry["resource"]["location"][0]["reference"]
641+
returned_location = self.client.get(location_url).data
642+
self.assertEqual(zip_search, returned_location["address"]["postalCode"])
641643

642-
self.assertEqual(zip_search, location_obj.address.address_us.zipcode)
643-
644644
def test_list_filter_by_address_general_zip_leading_zero(self):
645-
zip_search = "404 Great Amazing Avenue San Diego CA 05555"
645+
address_line_1 = "404 Great Amazing Avenue"
646+
city = "San Diego"
647+
state = "CA"
648+
zip_code = "05555"
649+
address_search = " ".join([address_line_1, city, state, zip_code])
646650
url = reverse("fhir-practitionerrole-list")
647-
response = self.client.get(url, {"location_address": zip_search})
651+
response = self.client.get(url, {"location_address": address_search})
648652
self.assertEqual(response.status_code, status.HTTP_200_OK)
649653
assert_has_results(self, response)
650654

651655
bundle = response.data["results"]
652656

653657
for entry in bundle["entry"]:
654-
location_id = entry["resource"]["location"][0]["reference"].split("/")[-1]
655658
self.assertIn("resource", entry)
656659
location_entry = entry["resource"]
657660

658661
self.assertEqual(location_entry["resourceType"], "PractitionerRole")
659662
self.assertIn("id", location_entry)
660663
self.assertIn("active", location_entry)
661664

662-
location_obj = Location.objects.get(pk=location_id)
663-
664-
self.assertEqual(zip_search.split()[-1], location_obj.address.address_us.zipcode)
665-
self.assertEqual("404 Great Amazing Avenue", location_obj.address.address_us.delivery_line_1)
666-
self.assertEqual("San Diego", location_obj.address.address_us.city_name)
667-
self.assertEqual("CA", location_obj.address.address_us.state_code.abbreviation)
665+
for entry in bundle["entry"]:
666+
self.assertEqual(1, len(entry["resource"]["location"]))
667+
location_url = entry["resource"]["location"][0]["reference"]
668+
returned_location = self.client.get(location_url).data
669+
self.assertEqual(zip_code, returned_location["address"]["postalCode"])
670+
self.assertEqual(state, returned_location["address"]["state"])
671+
self.assertEqual(city, returned_location["address"]["city"])
672+
self.assertIn(address_line_1, returned_location["address"]["line"])

0 commit comments

Comments
 (0)