Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-07-23 - Context Lexicon Regex Compilation Bottleneck
**Learning:** Deterministic lexicon processing (like negation and temporality cues) creates massive regular expression alternations across clinical pipelines. Re-compiling these every time `_compiled_context_lexicon` was called caused a significant CPU bottleneck.
**Action:** Use `@functools.lru_cache` to cache compiled regular expressions and lexicons. Ensure that arguments to these functions (e.g. `cues`) are typed as `tuple` rather than unhashable `Iterable` to be compatible with caching.
36 changes: 27 additions & 9 deletions openmed/openmed/clinical/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

from __future__ import annotations

import functools
import re
from collections.abc import Iterable, Iterator, Mapping, Sequence
from dataclasses import dataclass, replace
Expand Down Expand Up @@ -99,10 +100,10 @@
PSEUDO_NEGATION_CUES = _ENGLISH_CONTEXT_LEXICON.pseudo_negation


def _cue_pattern(
cues: Iterable[str],
*,
token_boundaries: bool = True,
@functools.lru_cache(maxsize=16)
def _cached_cue_pattern(
cues: tuple[str, ...],
token_boundaries: bool,
) -> re.Pattern[str]:
alternation = _cue_alternation(cues)
if not alternation:
Expand All @@ -114,17 +115,25 @@ def _cue_pattern(
return re.compile(pattern, re.IGNORECASE)


def _cue_alternation(cues: Iterable[str]) -> str:
def _cue_pattern(
cues: Iterable[str],
*,
token_boundaries: bool = True,
) -> re.Pattern[str]:
return _cached_cue_pattern(tuple(cues), token_boundaries)


def _cue_alternation(cues: tuple[str, ...]) -> str:
return "|".join(
r"\s+".join(re.escape(part) for part in cue.split())
for cue in sorted(set(cues), key=len, reverse=True)
)


def _terminator_pattern(
cues: Iterable[str],
*,
token_boundaries: bool = True,
@functools.lru_cache(maxsize=16)
def _cached_terminator_pattern(
cues: tuple[str, ...],
token_boundaries: bool,
) -> re.Pattern[str]:
alternation = _cue_alternation(cues)
punctuation = r"[.!?;。!?;]"
Expand All @@ -137,6 +146,14 @@ def _terminator_pattern(
return re.compile(rf"(?:{punctuation}|{cue_pattern})", re.IGNORECASE)


def _terminator_pattern(
cues: Iterable[str],
*,
token_boundaries: bool = True,
) -> re.Pattern[str]:
return _cached_terminator_pattern(tuple(cues), token_boundaries)


@dataclass(frozen=True)
class _CompiledContextLexicon:
language: str
Expand All @@ -154,6 +171,7 @@ class _CompiledContextLexicon:
backward_context_cues: frozenset[str]


@functools.lru_cache(maxsize=16)
def _compiled_context_lexicon(language: str | None = None) -> _CompiledContextLexicon:
lexicon = get_clinical_cue_lexicon(language)
token_boundaries = lexicon.token_boundaries
Expand Down