Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace littletable with redis-om(redisearch) #1788

Merged
merged 28 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3b1b496
replace little table with redis-om
sainak Dec 27, 2023
f4159aa
add partial word search
sainak Dec 27, 2023
77cecbc
limit prefix expansion to 3 words
sainak Dec 28, 2023
0cbf284
use redis-stack inplace of redis image
sainak Dec 28, 2023
d8cfae1
handle exceptions raised due to cache miss
sainak Dec 28, 2023
9741379
add health check for containers
sainak Dec 28, 2023
e3374f1
fix page size issue
sainak Dec 28, 2023
afe656d
fix test
sainak Dec 28, 2023
d034053
escape tokens
sainak Dec 28, 2023
29bf65e
Merge remote-tracking branch 'origin/master' into sainak/feat/redis-s…
sainak Dec 29, 2023
aba104d
Merge commit 'ef73d9368fc8df775688c497acbd4e17fd9ba1a2' into sainak/f…
sainak Jan 8, 2024
6c46084
add is leaf filter for icd11 items
sainak Jan 8, 2024
9f0d459
fix bucket auth
sainak Jan 8, 2024
19c17b4
fix localstack cors
sainak Jan 8, 2024
a584016
Merge remote-tracking branch 'origin/master' into sainak/feat/redis-s…
sainak Jan 9, 2024
1d67219
Merge remote-tracking branch 'origin/master' into sainak/feat/redis-s…
sainak Jan 10, 2024
d389f1e
use a different endpoint for docker health check
sainak Jan 10, 2024
5c9c5d5
Merge commit 'a92b60a8382d5011589e549773584039dbc54b74' into sainak/f…
sainak Jan 12, 2024
e83c961
Merge commit '7d0dd826f72973a856c0bdcd9fd5c495eb3b554c' into sainak/f…
sainak Jan 17, 2024
e067246
only load icd11 in cache which has code
sainak Jan 17, 2024
447bc1d
fix imports
sainak Jan 17, 2024
f591e5e
fix key error
sainak Jan 17, 2024
954f90d
revert docker changes
sainak Jan 17, 2024
7d4aa6f
fix ping endpoint
sainak Jan 17, 2024
5ac191c
fix icd11 search by code
sainak Jan 17, 2024
f685607
fix not found error handling
sainak Jan 17, 2024
cce054e
make icd tests cover more cases
sainak Jan 17, 2024
0341a96
fix ping endpoint
sainak Jan 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test-base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ hashFiles('Pipfile.lock', 'docker/prod.Dockerfile') }}
key: ${{ runner.os }}-buildx-${{ hashFiles('Pipfile.lock', 'docker/dev.Dockerfile') }}
restore-keys: |
${{ runner.os }}-buildx-

Expand All @@ -30,7 +30,7 @@ jobs:
files: docker-compose.yaml,docker-compose.local.yaml

- name: Start services
run: docker compose -f docker-compose.yaml -f docker-compose.local.yaml up -d --no-build
run: docker compose -f docker-compose.yaml -f docker-compose.local.yaml up -d --wait --no-build

- name: Check migrations
run: make checkmigration
Expand Down
10 changes: 5 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ build:
docker compose -f docker-compose.yaml -f $(docker_config_file) build

up:
docker compose -f docker-compose.yaml -f $(docker_config_file) up -d
docker compose -f docker-compose.yaml -f $(docker_config_file) up -d --wait

down:
docker compose -f docker-compose.yaml -f $(docker_config_file) down
Expand All @@ -34,16 +34,16 @@ list:
logs:
docker compose -f docker-compose.yaml -f $(docker_config_file) logs

checkmigration: up
checkmigration:
docker compose exec backend bash -c "python manage.py makemigrations --check --dry-run"

makemigrations: up
makemigrations:
docker compose exec backend bash -c "python manage.py makemigrations"

test: up
test:
docker compose exec backend bash -c "python manage.py test --keepdb --parallel"

test-coverage: up
test-coverage:
docker compose exec backend bash -c "coverage run manage.py test --settings=config.settings.test --keepdb --parallel"
docker compose exec backend bash -c "coverage combine || true; coverage xml"
docker compose cp backend:/app/coverage.xml coverage.xml
4 changes: 2 additions & 2 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ gunicorn = "==21.2.0"
healthy-django = "==0.1.0"
jsonschema = "==4.20.0"
jwcrypto = "==1.5.1"
littletable = "==2.2.3"
newrelic = "==9.3.0"
pillow = "==10.1.0"
psycopg = "==3.1.14"
Expand All @@ -41,10 +40,11 @@ pydantic = "==1.10.12" # fix for fhir.resources < 7.0.2
pyjwt = "==2.8.0"
python-slugify = "==8.0.1"
pywebpush = "==1.14.0"
redis = {extras = ["hiredis"], version = "==5.0.0"}
redis = {extras = ["hiredis"], version = "<5.0.0"} # constraint for redis-om
requests = "==2.31.0"
sentry-sdk = "==1.30.0"
whitenoise = "==6.5.0"
redis-om = "==0.2.1"

[dev-packages]
black = "==23.9.1"
Expand Down
692 changes: 370 additions & 322 deletions Pipfile.lock

Large diffs are not rendered by default.

39 changes: 18 additions & 21 deletions care/facility/api/viewsets/icd.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
from re import IGNORECASE

from redis_om import FindQuery
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet


def serailize_data(icd11_object):
result = []
for object in icd11_object:
if type(object) == tuple:
object = object[0]
result.append(
{"id": object.id, "label": object.label, "chapter": object.chapter}
)
return result
from care.facility.static_data.icd11 import ICD11
from care.utils.static_data.helpers import query_builder


class ICDViewSet(ViewSet):
permission_classes = (IsAuthenticated,)

def serialize_data(self, objects: list[ICD11]):
return [diagnosis.get_representation() for diagnosis in objects]

def list(self, request):
from care.facility.static_data.icd11 import ICDDiseases
try:
limit = min(int(request.query_params.get("limit")), 20)
except (ValueError, TypeError):
limit = 20

Check warning on line 20 in care/facility/api/viewsets/icd.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/icd.py#L17-L20

Added lines #L17 - L20 were not covered by tests

query = []

Check warning on line 22 in care/facility/api/viewsets/icd.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/icd.py#L22

Added line #L22 was not covered by tests
if q := request.query_params.get("query"):
query.append(ICD11.label % query_builder(q))

Check warning on line 24 in care/facility/api/viewsets/icd.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/icd.py#L24

Added line #L24 was not covered by tests

queryset = ICDDiseases
if request.GET.get("query", False):
query = request.GET.get("query")
queryset = queryset.where(
label=queryset.re_match(r".*" + query + r".*", IGNORECASE),
is_leaf=True,
) # can accept regex from FE if needed.
return Response(serailize_data(queryset[0:100]))
result = FindQuery(expressions=query, model=ICD11, limit=limit).execute(

Check warning on line 26 in care/facility/api/viewsets/icd.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/icd.py#L26

Added line #L26 was not covered by tests
exhaust_results=False
)
return Response(self.serialize_data(result))

Check warning on line 29 in care/facility/api/viewsets/icd.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/icd.py#L29

Added line #L29 was not covered by tests
67 changes: 20 additions & 47 deletions care/facility/api/viewsets/prescription.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.utils import timezone
from django_filters import rest_framework as filters
from drf_spectacular.utils import extend_schema
from redis_om import FindQuery
from rest_framework import mixins, status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
Expand All @@ -18,8 +19,10 @@
PrescriptionType,
generate_choices,
)
from care.facility.static_data.medibase import MedibaseMedicine
from care.utils.filters.choicefilter import CareChoiceFilter
from care.utils.queryset.consultation import get_consultation_queryset
from care.utils.static_data.helpers import query_builder, token_escaper


def inverse_choices(choices):
Expand Down Expand Up @@ -150,57 +153,27 @@
class MedibaseViewSet(ViewSet):
permission_classes = (IsAuthenticated,)

def serailize_data(self, objects):
return [
{
"id": x[0],
"name": x[1],
"type": x[2],
"generic": x[3],
"company": x[4],
"contents": x[5],
"cims_class": x[6],
"atc_classification": x[7],
}
for x in objects
]

def sort(self, query, results):
exact_matches = []
word_matches = []
partial_matches = []

for x in results:
name = x[1].lower()
generic = x[3].lower()
company = x[4].lower()
words = f"{name} {generic} {company}".split()

if name == query:
exact_matches.append(x)
elif query in words:
word_matches.append(x)
else:
partial_matches.append(x)

return exact_matches + word_matches + partial_matches
def serialize_data(self, objects: list[MedibaseMedicine]):
return [medicine.get_representation() for medicine in objects]

def list(self, request):
from care.facility.static_data.medibase import MedibaseMedicineTable

queryset = MedibaseMedicineTable
try:
limit = min(int(request.query_params.get("limit")), 30)
except (ValueError, TypeError):
limit = 30

query = []
if type := request.query_params.get("type"):
queryset = [x for x in queryset if x[2] == type]
query.append(MedibaseMedicine.type == type)

Check warning on line 167 in care/facility/api/viewsets/prescription.py

View check run for this annotation

Codecov / codecov/patch

care/facility/api/viewsets/prescription.py#L167

Added line #L167 was not covered by tests

if query := request.query_params.get("query"):
query = query.strip().lower()
queryset = [x for x in queryset if query in f"{x[1]} {x[3]} {x[4]}".lower()]
queryset = self.sort(query, queryset)
if q := request.query_params.get("query"):
query.append(
(MedibaseMedicine.name == token_escaper.escape(q))
| (MedibaseMedicine.vec % query_builder(q))
)

try:
limit = min(int(request.query_params.get("limit", 30)), 100)
except ValueError:
limit = 30
result = FindQuery(
expressions=query, model=MedibaseMedicine, limit=limit
).execute(exhaust_results=False)

return Response(self.serailize_data(queryset[:limit]))
return Response(self.serialize_data(result))
28 changes: 28 additions & 0 deletions care/facility/management/commands/load_redis_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.core.cache import cache
from django.core.management import BaseCommand

Check warning on line 2 in care/facility/management/commands/load_redis_index.py

View check run for this annotation

Codecov / codecov/patch

care/facility/management/commands/load_redis_index.py#L1-L2

Added lines #L1 - L2 were not covered by tests

from care.facility.static_data.icd11 import load_icd11_diagnosis
from care.facility.static_data.medibase import load_medibase_medicines
from care.hcx.static_data.pmjy_packages import load_pmjy_packages

Check warning on line 6 in care/facility/management/commands/load_redis_index.py

View check run for this annotation

Codecov / codecov/patch

care/facility/management/commands/load_redis_index.py#L4-L6

Added lines #L4 - L6 were not covered by tests


class Command(BaseCommand):

Check warning on line 9 in care/facility/management/commands/load_redis_index.py

View check run for this annotation

Codecov / codecov/patch

care/facility/management/commands/load_redis_index.py#L9

Added line #L9 was not covered by tests
"""
Command to load static data to redis
Usage: python manage.py load_redis_index
"""

help = "Loads static data to redis"

Check warning on line 15 in care/facility/management/commands/load_redis_index.py

View check run for this annotation

Codecov / codecov/patch

care/facility/management/commands/load_redis_index.py#L15

Added line #L15 was not covered by tests

def handle(self, *args, **options):

Check warning on line 17 in care/facility/management/commands/load_redis_index.py

View check run for this annotation

Codecov / codecov/patch

care/facility/management/commands/load_redis_index.py#L17

Added line #L17 was not covered by tests
if cache.get("redis_index_loading"):
print("Redis Index already loading, skipping")
return

Check warning on line 20 in care/facility/management/commands/load_redis_index.py

View check run for this annotation

Codecov / codecov/patch

care/facility/management/commands/load_redis_index.py#L19-L20

Added lines #L19 - L20 were not covered by tests

cache.set("redis_index_loading", True, timeout=60 * 5)

Check warning on line 22 in care/facility/management/commands/load_redis_index.py

View check run for this annotation

Codecov / codecov/patch

care/facility/management/commands/load_redis_index.py#L22

Added line #L22 was not covered by tests

load_icd11_diagnosis()
load_medibase_medicines()
load_pmjy_packages()

Check warning on line 26 in care/facility/management/commands/load_redis_index.py

View check run for this annotation

Codecov / codecov/patch

care/facility/management/commands/load_redis_index.py#L24-L26

Added lines #L24 - L26 were not covered by tests

cache.delete("redis_index_loading")

Check warning on line 28 in care/facility/management/commands/load_redis_index.py

View check run for this annotation

Codecov / codecov/patch

care/facility/management/commands/load_redis_index.py#L28

Added line #L28 was not covered by tests
108 changes: 68 additions & 40 deletions care/facility/static_data/icd11.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,73 @@
import contextlib
from typing import TypedDict

from django.db import connection
from littletable import Table
from django.core.paginator import Paginator
from redis_om import Field, Migrator

from care.facility.models.icd11_diagnosis import ICD11Diagnosis
from care.utils.static_data.models.base import BaseRedisModel


def fetch_from_db():
# This is a hack to prevent the migration from failing when the table does not exist
all_tables = connection.introspection.table_names()
if "facility_icd11diagnosis" in all_tables:
return [
{
"id": str(diagnosis["id"]),
"label": diagnosis["label"],
"is_leaf": diagnosis["is_leaf"],
"chapter": diagnosis["meta_chapter_short"],
}
for diagnosis in ICD11Diagnosis.objects.filter().values(
"id", "label", "is_leaf", "meta_chapter_short"
)
]
return []


ICDDiseases = Table("ICD11")
ICDDiseases.insert_many(fetch_from_db())
ICDDiseases.create_search_index("label")
ICDDiseases.create_index("id", unique=True)


def get_icd11_diagnosis_object_by_id(diagnosis_id, as_dict=False):
obj = None
with contextlib.suppress(BaseException):
obj = ICDDiseases.by.id[str(diagnosis_id)]
return obj and (obj.__dict__ if as_dict else obj)


def get_icd11_diagnoses_objects_by_ids(diagnoses_ids):
diagnosis_objects = []
for diagnosis in diagnoses_ids:
with contextlib.suppress(BaseException):
diagnosis_objects.append(ICDDiseases.by.id[str(diagnosis)].__dict__)
return diagnosis_objects
class ICD11Object(TypedDict):
id: int
label: str
is_leaf: bool
chapter: str


class ICD11(BaseRedisModel):
id: int = Field(primary_key=True)
label: str = Field(index=True, full_text_search=True)
is_leaf: int = Field(index=True)
chapter: str

def get_representation(self) -> ICD11Object:
return {
"id": self.id,
"label": self.label,
"is_leaf": bool(self.is_leaf),
"chapter": self.chapter,
}


def load_icd11_diagnosis():
print("Loading ICD11 Diagnosis into the redis cache...", end="", flush=True)

Check warning on line 33 in care/facility/static_data/icd11.py

View check run for this annotation

Codecov / codecov/patch

care/facility/static_data/icd11.py#L33

Added line #L33 was not covered by tests

icd_objs = ICD11Diagnosis.objects.order_by("id").values_list(

Check warning on line 35 in care/facility/static_data/icd11.py

View check run for this annotation

Codecov / codecov/patch

care/facility/static_data/icd11.py#L35

Added line #L35 was not covered by tests
"id", "label", "is_leaf", "meta_chapter_short"
)
paginator = Paginator(icd_objs, 5000)

Check warning on line 38 in care/facility/static_data/icd11.py

View check run for this annotation

Codecov / codecov/patch

care/facility/static_data/icd11.py#L38

Added line #L38 was not covered by tests
for page_number in paginator.page_range:
for diagnosis in paginator.page(page_number).object_list:
ICD11(

Check warning on line 41 in care/facility/static_data/icd11.py

View check run for this annotation

Codecov / codecov/patch

care/facility/static_data/icd11.py#L41

Added line #L41 was not covered by tests
id=diagnosis[0],
label=diagnosis[1],
is_leaf=int(diagnosis[2] or 0),
chapter=diagnosis[3] or "",
).save()
Migrator().run()
print("Done")

Check warning on line 48 in care/facility/static_data/icd11.py

View check run for this annotation

Codecov / codecov/patch

care/facility/static_data/icd11.py#L47-L48

Added lines #L47 - L48 were not covered by tests


def get_icd11_diagnosis_object_by_id(
diagnosis_id: int, as_dict=False
) -> ICD11 | ICD11Object | None:
try:
diagnosis = ICD11.get(diagnosis_id)
return diagnosis.get_representation() if as_dict else diagnosis
except KeyError:
return None

Check warning on line 58 in care/facility/static_data/icd11.py

View check run for this annotation

Codecov / codecov/patch

care/facility/static_data/icd11.py#L57-L58

Added lines #L57 - L58 were not covered by tests


def get_icd11_diagnoses_objects_by_ids(diagnoses_ids: list[int]) -> list[ICD11Object]:
if not diagnoses_ids:
return []

Check warning on line 63 in care/facility/static_data/icd11.py

View check run for this annotation

Codecov / codecov/patch

care/facility/static_data/icd11.py#L63

Added line #L63 was not covered by tests

query = None
for diagnosis_id in diagnoses_ids:
if query is None:
query = ICD11.id == diagnosis_id
else:
query |= ICD11.id == diagnosis_id

Check warning on line 70 in care/facility/static_data/icd11.py

View check run for this annotation

Codecov / codecov/patch

care/facility/static_data/icd11.py#L70

Added line #L70 was not covered by tests

diagnosis_objects: list[ICD11] = list(ICD11.find(query))
return [diagnosis.get_representation() for diagnosis in diagnosis_objects]
Loading
Loading