Skip to content

Commit d800133

Browse files
authored
Added search-based recommendations to auto_mapping DAG. (#1159)
1 parent 024017e commit d800133

4 files changed

Lines changed: 163 additions & 3 deletions

File tree

app/airflow/dags/auto_mapping_dag.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424
find_additional_fields,
2525
)
2626
from libs.auto_mapping.core_rules_creation import create_mapping_rules
27+
from libs.auto_mapping.search_recommendations import process_search_recommendations
2728
from libs.utils import create_task, validate_params_auto_mapping
28-
from libs.settings import AIRFLOW_DEBUG_MODE, AIRFLOW_DAGRUN_TIMEOUT
29+
from libs.settings import AIRFLOW_DEBUG_MODE, SEARCH_ENABLED, AIRFLOW_DAGRUN_TIMEOUT
2930

3031
"""
3132
This DAG automates the process of creating and reusing concepts from scan reports and generating mapping rules.
@@ -43,6 +44,7 @@
4344
10. Find concept fields for mapping
4445
11. Find additional fields needed for mapping
4546
12. Create mapping rules based on all collected information
47+
13. (Optional) Generate search-based recommendations when SEARCH_ENABLED=true
4648
4749
This pipeline enables efficient concept reuse across scan reports and automates the creation of
4850
mapping rules connecting source data to OMOP-compliant destination tables.
@@ -71,8 +73,14 @@
7173
default_args=default_args,
7274
description="""Find and create V and R concepts. Then get all the existing concepts,
7375
and find the dest. table and OMOP field ids for each concept.
74-
After that, create mapping rules for each concept.""",
75-
tags=["V-concepts", "R-concepts", "mapping_rules_creation"],
76+
After that, create mapping rules for each concept.
77+
Optionally includes search-based recommendations when SEARCH_ENABLED=true.""",
78+
tags=[
79+
"V-concepts",
80+
"R-concepts",
81+
"mapping_rules_creation",
82+
"search_recommendations",
83+
],
7684
schedule_interval=None,
7785
catchup=False,
7886
is_paused_upon_creation=False,
@@ -103,6 +111,13 @@
103111
create_task("create_mapping_rules", create_mapping_rules, dag),
104112
]
105113

114+
# Conditionally add search recommendations task
115+
if SEARCH_ENABLED == "true":
116+
tasks.append(
117+
create_task(
118+
"process_search_recommendations", process_search_recommendations, dag
119+
)
120+
)
106121

107122
# End the workflow
108123
end = EmptyOperator(task_id="end", dag=dag)
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from libs.utils import (
2+
pull_validated_params,
3+
)
4+
from airflow.providers.postgres.hooks.postgres import PostgresHook
5+
import logging
6+
7+
# PostgreSQL connection hook
8+
pg_hook = PostgresHook(postgres_conn_id="postgres_db_conn")
9+
10+
11+
def process_search_recommendations(**kwargs) -> None:
12+
"""
13+
Process search recommendations for a given scan report table.
14+
15+
This function:
16+
1. Retrieves scan report value strings and searches the OMOP concept table using ILIKE queries
17+
2. Selects the top 3 matches for each value
18+
3. Inserts them as MappingRecommendations into the database
19+
20+
Parameters are validated upstream in validate_params_auto_mapping task.
21+
"""
22+
# Get validated parameters from XCom
23+
validated_params = pull_validated_params(kwargs, "validate_params_auto_mapping")
24+
25+
table_id = validated_params["table_id"]
26+
27+
try:
28+
# Get content type for ScanReportValue
29+
content_type_query = """
30+
SELECT id FROM django_content_type
31+
WHERE app_label = 'mapping' AND model = 'scanreportvalue'
32+
LIMIT 1
33+
"""
34+
content_type_result = pg_hook.get_first(content_type_query)
35+
if not content_type_result:
36+
raise ValueError("Could not find content type for ScanReportValue")
37+
content_type_id = content_type_result[0]
38+
39+
# Get all scan report values for the table
40+
get_values_query = """
41+
SELECT
42+
sr_value.id,
43+
sr_value.value
44+
FROM mapping_scanreportvalue sr_value
45+
JOIN mapping_scanreportfield sr_field ON sr_value.scan_report_field_id = sr_field.id
46+
WHERE sr_field.scan_report_table_id = %(table_id)s
47+
AND sr_value.value IS NOT NULL
48+
AND TRIM(sr_value.value) <> ''
49+
"""
50+
51+
logging.info(f"Getting scan report values for table_id: {table_id}")
52+
values = pg_hook.get_records(
53+
get_values_query, parameters={"table_id": table_id}
54+
)
55+
56+
if not values:
57+
logging.info(f"No values found for table {table_id}")
58+
return
59+
60+
# Process each value with individual queries
61+
# This is more efficient for large concept tables because:
62+
# 1. Each query has LIMIT 3, stopping early
63+
# 2. No cross-join with 1.9M concept records
64+
# 3. Database can optimize each focused query
65+
recommendations_created = 0
66+
for value_id, value_string in values:
67+
try:
68+
# Search OMOP concepts using ILIKE with LIMIT 3
69+
# This is more efficient than cross-join approach
70+
search_query = """
71+
SELECT concept_id
72+
FROM omop.concept
73+
WHERE concept_name ILIKE %(search_term)s
74+
AND invalid_reason IS NULL
75+
AND standard_concept = 'S'
76+
ORDER BY concept_name
77+
LIMIT 3
78+
"""
79+
80+
matches = pg_hook.get_records(
81+
search_query, parameters={"search_term": f"%{value_string}%"}
82+
)
83+
84+
if matches:
85+
# Insert recommendations for each match
86+
for (concept_id,) in matches:
87+
insert_query = """
88+
INSERT INTO mapping_mappingrecommendation (
89+
content_type_id,
90+
object_id,
91+
concept_id,
92+
score,
93+
tool_name,
94+
tool_version,
95+
created_at,
96+
updated_at
97+
) VALUES (
98+
%(content_type_id)s,
99+
%(object_id)s,
100+
%(concept_id)s,
101+
%(score)s,
102+
%(tool_name)s,
103+
%(tool_version)s,
104+
NOW(),
105+
NOW()
106+
)
107+
"""
108+
109+
pg_hook.run(
110+
insert_query,
111+
parameters={
112+
"content_type_id": content_type_id,
113+
"object_id": value_id,
114+
"concept_id": concept_id,
115+
"score": 0.5, # Base score for ILIKE matches
116+
"tool_name": "string-search",
117+
"tool_version": "1.0.0",
118+
},
119+
)
120+
121+
recommendations_created += 1
122+
123+
logging.info(
124+
f"Created {len(matches)} recommendations for value '{value_string}'"
125+
)
126+
else:
127+
logging.info(f"No matches found for value '{value_string}'")
128+
129+
except Exception as e:
130+
logging.error(f"Error processing value '{value_string}': {str(e)}")
131+
# Continue with next value instead of failing the entire process
132+
continue
133+
134+
logging.info(
135+
f"Successfully created {recommendations_created} search recommendations"
136+
)
137+
138+
except Exception as e:
139+
logging.error(f"Error in process_search_recommendations: {str(e)}")
140+
raise

app/airflow/dags/libs/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@
1010
# DEBUG MODE: True or False
1111
AIRFLOW_DEBUG_MODE = os.getenv("AIRFLOW_DEBUG_MODE", "false").lower()
1212

13+
# SEARCH ENABLED: Controls whether search recommendations DAG is enabled
14+
SEARCH_ENABLED = os.getenv("SEARCH_ENABLED", "false").lower()
15+
1316
# Timedelta for dagrun_timeout in minutes
1417
AIRFLOW_DAGRUN_TIMEOUT = os.getenv("AIRFLOW_DAGRUN_TIMEOUT", 60)

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ services:
252252
<<: *airflow-common-env
253253
AIRFLOW__CORE__EXECUTE_TASKS_NEW_PYTHON_INTERPRETER: True
254254
AIRFLOW_DEBUG_MODE: False
255+
# Search recommendations feature flag - controls whether search recommendations task is included in auto_mapping DAG
256+
SEARCH_ENABLED: "true"
255257
AIRFLOW_DAGRUN_TIMEOUT: 60 # Timeout for each dagrun in minutes
256258

257259
volumes:

0 commit comments

Comments
 (0)