From 1fff8e5987feaa8f03001eeae8ecdb74a47b3cc5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:28:19 +0000 Subject: [PATCH] Add caching to lexicon regex compilations Co-authored-by: zrt219 <199104500+zrt219@users.noreply.github.com> --- .jules/bolt.md | 3 +++ openmed/openmed/clinical/context.py | 36 +++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..09c307e --- /dev/null +++ b/.jules/bolt.md @@ -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. diff --git a/openmed/openmed/clinical/context.py b/openmed/openmed/clinical/context.py index 9fd11df..810bacb 100644 --- a/openmed/openmed/clinical/context.py +++ b/openmed/openmed/clinical/context.py @@ -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 @@ -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: @@ -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"[.!?;。!?;]" @@ -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 @@ -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