Skip to content

Repository files navigation

structflo.ner

structflo.ner logo

PyPI DownloadsTestsCoverageLicenseLinkedInGitHub

Zero-config Named Entity Recognition for drug discovery, chemistry, and biological sciences.

InstallationLLM ExtractionFast NERProfilesVisualizationNotebooks


structflo.ner is a lightweight NER library specialized for pharmaceutical and biological sciences. It uses LangExtract and fuzzy based tools to deliver zero-configuration entity extraction.

It ships with two extraction engines:

NERExtractorFastNERExtractor
ApproachLLM-powered (Gemini, Ollama)Dictionary-based (YAML gazetteers)
Speed~10-60s per abstract~0.4-1s per abstract
Novel entitiesDiscovers new entitiesKnown terms only
Context awarenessFull contextual understandingString matching (exact + fuzzy)
CostAPI costs or local GPUFree (no API calls)
SetupAPI key or OllamaZero config
Output formatNERResultNERResult (identical)

Installation

pip install structflo-ner
# or with uv
uv add structflo-ner

Install optional extras as needed:

pip install "structflo-ner[dataframe]"# pandas DataFrame support
pip install "structflo-ner[fast]"# fast dictionary-based NER (rapidfuzz)

LLM-Powered Extraction

Cloud model (Gemini)

The default model is gemini-2.5-flash. Pass your API key or set the GEMINI_API_KEY environment variable.

fromstructflo.nerimportNERExtractorextractor=NERExtractor(api_key="YOUR_GEMINI_KEY")
result=extractor.extract(
"Gefitinib (ZD1839) is a first-generation EGFR tyrosine kinase inhibitor ""with IC50 = 0.033 µM, approved for non-small cell lung cancer (NSCLC). ""Its SMILES is COc1cc2ncnc(Nc3ccc(F)c(Cl)c3)c2cc1OCCCN1CCOCC1."
)

Local models via Ollama

Run extraction entirely on your own hardware — no API key needed:

extractor=NERExtractor(
model_id="qwen2.5:72b",
model_url="http://localhost:11434",
)
text= ("Gefitinib (ZD1839) is a first-generation EGFR inhibitor with IC50 = 0.033 µM approved for NSCLC.""Its SMILES is COc1cc2ncnc(Nc3ccc(F)c(Cl)c3)c2cc1OCCCN1CCOCC1.")
result=extractor.extract(text)
result

Any model served by Ollama works gemma, llama, mistral, qwen, deepseek, etc.

Render results as color-coded, interactive HTML directly in Jupyter notebooks: Results

To get a PANDAS dataframe.

result.to_dataframe()

Results

TB specific extractor pass in the profile=TB

fromstructflo.nerimportNERExtractor, TBextractor=NERExtractor(
model_id="qwen2.5:72b",
model_url="http://localhost:11434",
profile=TB,
text= (
"Bedaquiline (TMC207) is a diarylquinoline that inhibits the ""mycobacterial ATP synthase subunit c encoded by atpE (Rv1305). ""It shows potent activity against Mycobacterium tuberculosis ""including MDR-TB and XDR-TB. This compound was identified through ""whole-cell screening and targets the energy metabolism pathway."
)
result=extractor.extract(text)
result

Results

# Flat list of all entitiesforentityinresult.all_entities():
print(f"{entity.entity_type:20s} | {entity.text}")
compound_name | Bedaquiline
compound_name | TMC207
target | ATP synthase subunit c
disease | MDR-TB
disease | XDR-TB
accession_number | Rv1305
functional_category | energy metabolism pathway
screening_method | whole-cell screening

Batch extraction

Pass a list of texts to extract from multiple documents at once:

texts= [
"Imatinib inhibits BCR-ABL with IC50 = 0.6 µM in CML.",
"Trastuzumab targets HER2 in breast cancer patients.",
"Remdesivir (GS-5734) is an antiviral with EC50 = 0.77 µM against SARS-CoV-2.",
]
results=extractor.extract(texts)
--- Text 1 ---
compound_name | Imatinib
target | BCR-ABL
disease | CML
bioactivity | IC50 = 0.6 µM
--- Text 2 ---
compound_name | Trastuzumab
target | HER2
disease | breast cancer
--- Text 3 ---
compound_name | Remdesivir
compound_name | GS-5734
disease | SARS-CoV-2
bioactivity | EC50 = 0.77 µM

Fast Dictionary-Based NER (Mode 2)

FastNERExtractor uses curated YAML gazetteers with a three-phase matching strategy for deterministic, high-speed extraction when LLMs are not available. These run extremely fast, however they are fuzzy based matches to predefined patterns and so it does not understands context.

fromstructflo.ner.fastimportFastNERExtractorfast=FastNERExtractor()
text= (
"Bedaquiline (TMC207) is a diarylquinoline that inhibits the ""mycobacterial ATP synthase subunit c encoded by atpE (Rv1305). ""It shows potent activity against Mycobacterium tuberculosis ""including MDR-TB and XDR-TB. This compound was identified through ""whole-cell screening and targets the energy metabolism pathway."
)
result=fast.extract(text)
result

Results

How matching works

PhaseMethodWhat it catches
1Exact matchCase-sensitive and normalized dictionary lookups with word-boundary enforcement
1bRegex patternsAuto-derived patterns from accession number seeds (Rv tags, UniProt, PDB, etc.)
2Fuzzy matchTypos and minor variants via rapidfuzz (configurable threshold)
# Fuzzy matching catches typosresult=fast.extract("Bedaquilne showed activity against TB")
# "Bedaquilne" -> canonical: "Bedaquiline" (method: fuzzy)# Disable fuzzy matching for strict modestrict=FastNERExtractor(fuzzy_threshold=0)

Built-in gazetteers

The fast extractor ships with curated gazetteers for TB drug discovery:

GazetteerExamples
accession_numberRv1305, B586_RS00005
gene_nameatpE, InhA, DprE1
screening_methodwhole-cell screening, fragment-based screening
targetInhA, DprE1, MmpL3
compound_nameBedaquiline, Delamanid, Pretomanid
functional_categoryDNA replication, cell wall biosynthesis
strainM. tuberculosis H37Rv
productenoyl-ACP reductase, ATP synthase subunit c
diseaseTB, MDR-TB, XDR-TB

Custom gazetteers

Extend the built-in dictionaries with your own terms:

custom=FastNERExtractor(
extra_gazetteers={
"target": ["MyNovelTarget", "KinaseX"],
"compound_name": ["CompoundABC"],
}
)

Or drop a new YAML file into the gazetteers directory — the filename (without .yml) maps to an entity type.

Performance

Single abstract: ~393 ms
8 abstracts: ~862 ms

Profiles

Profiles control which entity types are extracted. Use them to focus the model on specific categories.

Built-in profiles

ProfileEntity classes
FULL (default)compounds, targets, diseases, bioactivities, assays, mechanisms
CHEMISTRYcompound names, SMILES, CAS numbers, molecular formulas
BIOLOGYtargets, gene names, protein names
BIOACTIVITYbioactivity measurements, assays
DISEASEdiseases and clinical indications
TBTB drug discovery (compounds, targets, diseases, accessions, strains, screening methods, functional categories)
fromstructflo.nerimportNERExtractor, CHEMISTRYextractor=NERExtractor(api_key="YOUR_GEMINI_KEY")
result=extractor.extract(text, profile=CHEMISTRY)

Merging profiles

Combine multiple profiles for broader extraction:

fromstructflo.nerimportCHEMISTRY, BIOLOGYcombined=CHEMISTRY.merge(BIOLOGY)
result=extractor.extract(text, profile=combined)
# Profile: chemistry+biology# Entity classes: compound_name, smiles, cas_number, molecular_formula, target, gene_name, protein_name

Custom profiles

Define your own extraction schema:

fromstructflo.nerimportNERExtractor, EntityProfilemy_profile=EntityProfile(
name="kinase_inhibitors",
entity_classes=["compound_name", "smiles", "target", "bioactivity"],
prompt="Extract kinase inhibitor names, SMILES, targets, and potency values.",
examples=my_examples,
)
result=extractor.extract(text, profile=my_profile)

Working with Results

Both extractors return identical NERResult objects:

# Typed entity listsresult.compounds# [ChemicalEntity(...)]result.targets# [TargetEntity(...)]result.diseases# [DiseaseEntity(...)]result.bioactivities# [BioactivityEntity(...)]result.assays# [...]result.mechanisms# [...]result.accessions# [AccessionEntity(...)]# Flat list of all entitiesresult.all_entities()
# Export to pandas DataFramedf=result.to_dataframe()
# Serialize to dict (JSON-friendly)result.to_dict()

Notebooks

Explore worked examples in the notebooks/ directory:

NotebookDescription
01_quickstart.ipynbEnd-to-end extraction with cloud and local models, profiles, batch extraction
02_fast_ner.ipynbFast dictionary-based NER — matching strategies, custom gazetteers, performance

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

# clone and install dev dependencies
git clone https://github.com/structflo/structflo-ner.git
cd structflo-ner
pip install -e ".[dataframe]" --group dev
# run tests
pytest
# lint
ruff check .
ruff format .

Citation

If you use structflo.ner in your research, please cite:

BibTeX
@software{structflo_ner,
title = {structflo.ner: Zero-config NER for Drug Discovery},
url = {https://github.com/structflo/structflo-ner},
year = {2026}
}

License

This project is licensed under the Apache License 2.0.

About

Zero-config Named Entity Recognition for drug discovery, chemistry, and biological sciences.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages