Skip to main content

Pure Python triplet extraction based on Stanford OpenIE

Project description

triplet-extract

GPU-accelerated Python implementation of Stanford OpenIE with comprehensive triplet extraction

License: GPL v3 Python 3.10+

Example

from triplet_extract import extract

text = "95.6% of people don't know what GraphRAG is for"
triplets = extract(text)

for t in triplets:
    print(f"({t.subject}, {t.relation}, {t.object})")

Output:

(95.6% of people, don't know, what GraphRAG is for)

Features:

  • Comprehensive extraction using breadth-first search
  • Natural formatting with proper contraction spacing
  • Quantifiers preserved and normalized (percentages, scientific units)
  • LaTeX math preserved for scientific literature
  • Optional GPU acceleration for batch processing

About

This is a GPU-accelerated Python port of Stanford OpenIE that extends the original natural-logic pipeline with breadth-first search for comprehensive triplet extraction. The implementation follows the same three-stage pipeline and uses the trained models from the Stanford NLP Group's research.

Technical Approach

To our knowledge, this is the first open-source system that GPU-accelerates the natural-logic forward-entailment search itself — via batched reparsing over dependency parses — rather than replacing the natural-logic OpenIE pipeline with a neural model trained on its outputs.

Prior neural OpenIE models typically train on triplets produced by classical OpenIE systems, using GPUs for neural inference over those labels. In contrast, this system keeps the original natural-logic semantics and uses the GPU to accelerate the BFS exploration through batch processing, effectively GPU-accelerating the underlying OpenIE algorithm rather than approximating it with a neural model.

This port uses spaCy for dependency parsing instead of Stanford CoreNLP, providing a pure Python alternative that works without Java dependencies. I'm grateful to the Stanford NLP Group for their groundbreaking research and for making their models available.

Note: This implementation supports English text only. The trained models and natural logic rules are language-specific.

Design Philosophy

This implementation prioritizes preserving rich semantic context in extracted triplets. Unlike some ports that simplify subjects and relations, this port retains qualifiers, quantifiers, and contextual information (e.g., "The U.S. president Barack Obama" rather than just "Barack Obama", or "25% of people" rather than just "people"). This makes the output particularly well-suited for knowledge graph construction, GraphRAG applications, and other systems that benefit from semantically rich representations.

Installation

Recommended: GPU-accelerated (more comprehensive extraction):

pip install triplet-extract[deepsearch]
python -m spacy download en_core_web_sm

Requires: CUDA-capable GPU, CUDA 12.x, 8GB+ VRAM recommended Benefit: ~1.9x more triplets with GPU-accelerated BFS (vs default Balanced mode)

Base install (CPU-optimized):

pip install triplet-extract
python -m spacy download en_core_web_sm

Works on: Any machine, serverless, edge devices Performance: Fast CPU-optimized DFS (13.60/s)

Local development with uv:

git clone https://github.com/adlumal/triplet-extract.git
cd triplet-extract
uv venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
uv pip install -e ".[deepsearch]"
uv pip install -e ".[dev]"
uv run spacy download en_core_web_sm

Usage

Basic Extraction

from triplet_extract import extract

text = "Cats love milk and mice."
triplets = extract(text)

for t in triplets:
    print(f"({t.subject}, {t.relation}, {t.object})")

Using the Extractor Class

The OpenIEExtractor class provides more control over the extraction pipeline:

from triplet_extract import OpenIEExtractor

extractor = OpenIEExtractor(
    enable_clause_split=True,    # Split complex sentences into clauses
    enable_entailment=True,      # Generate entailed shorter forms
    min_confidence=0.5           # Filter low-confidence triplets
)

triplets = extractor.extract_triplet_objects(text)

for t in triplets:
    print(f"Subject: {t.subject}")
    print(f"Relation: {t.relation}")
    print(f"Object: {t.object}")
    print(f"Confidence: {t.confidence}")
    print()

Extraction Modes

The extractor uses Balanced mode by default, which is CPU-optimized for production use:

# Default: Balanced mode (CPU-optimized, 13.60/s)
extractor = OpenIEExtractor()

# Enable Deep Search for comprehensive extraction (GPU recommended)
# Automatically enables fast=True and speed_preset="fast"
extractor = OpenIEExtractor(deep_search=True)

# CPU speed presets (automatically enable fast=True)
extractor = OpenIEExtractor(speed_preset="fast")    # 17.22/s, fewer triplets
extractor = OpenIEExtractor(speed_preset="ultra")   # 28.22/s, minimal triplets

Performance comparison (100 scientific abstracts):

Mode Triplets/Sent Total (100s) Throughput Time (100s) Coverage† Precision Use Case
Deep Search 16.34 1634 8.86/s 11.29s 100% 98%+ Comprehensive extraction (GPU-accelerated)
Baseline (DFS) 7.93 793 1.96/s 51.09s 48.5% 100% Reference quality
Balanced (default) 8.55 855 13.60/s 7.35s 52.3% 98.2% Default: CPU-optimized production
Fast 6.57 657 17.22/s 5.81s 40.2% 98.4% High-throughput APIs
Ultra 5.21 521 28.22/s 3.54s 31.9% 99.5% Maximum speed
Stanford OpenIE 13.45 1345 15.42/s 6.48s 82.3% ~95% Original Java

†Coverage: Percentage of Deep Search triplets found (Deep Search finds the most = 100% baseline)

Note: Stanford OpenIE benchmarks executed via stanford-openie-python package. Numbers vary slightly between runs.

Benchmark Hardware:

  • GPU tests: NVIDIA RTX 5090 (32GB VRAM), CUDA 12.x
  • CPU tests: AMD Ryzen 7 9800X3D (8-Core, 16 threads), 48GB RAM
  • Dataset: 100 scientific abstracts (LaTeX-free)

Deployment Notes

GPU-Accelerated

Workstations, GPU servers, batch processing pipelines

BFS mode with CUDA acceleration - ~1.9x more triplets vs default Balanced mode:

Mode Configuration Hardware Use Case
Deep Search (GPU) deep_search=True GPU (CUDA) Comprehensive extraction, knowledge graphs
Deep Search (CPU fallback) deep_search=True CPU only Same quality, slower throughput

*Estimated based on BFS algorithm complexity. Actual performance varies by CPU.

GPU Requirements: CUDA 12.x, 8GB+ VRAM recommended

Example:

from triplet_extract import OpenIEExtractor

# GPU-accelerated comprehensive extraction
# Auto-detects optimal batch size based on VRAM (adds ~1s initialization)
extractor = OpenIEExtractor(deep_search=True)

# Process multiple texts efficiently with batching
texts = ["Sentence 1", "Sentence 2", "Sentence 3", ...]
results = extractor.extract_batch(texts)  # Returns list of triplet lists

# Disable auto-detection for faster initialization (use fixed batch size)
extractor = OpenIEExtractor(deep_search=True, gpu_batch_size=128)

CPU-Optimized (Default)

AWS Lambda, Cloud Run, serverless functions, edge devices

All DFS modes - optimized for CPU with LRU caching:

Mode Configuration Use Case
Balanced (recommended) deep_search=False, speed_preset="balanced" Production default
Fast deep_search=False, speed_preset="fast" High-throughput APIs
Ultra deep_search=False, speed_preset="ultra" Maximum speed priority
Baseline high_quality=True, fast=False, deep_search=False Reference/compatibility

Example:

from triplet_extract import OpenIEExtractor

# CPU-optimized for serverless deployment (default)
extractor = OpenIEExtractor()  # Uses balanced preset

# Process multiple texts efficiently
texts = ["Sentence 1", "Sentence 2", "Sentence 3", ...]
results = extractor.extract_batch(texts)  # 3-5x faster than individual calls

# Or adjust speed/quality tradeoff
extractor = OpenIEExtractor(speed_preset="fast")    # Higher throughput
extractor = OpenIEExtractor(speed_preset="ultra")   # Maximum speed

Pipeline Options

The extractor implements three stages:

Stage 1: Clause Splitting (enable_clause_split) Breaks complex sentences into simpler clauses using beam search. For example, "Obama, born in Hawaii, is president" becomes ["Obama is president", "Obama born in Hawaii"].

Stage 2: Forward Entailment (enable_entailment) Generates shorter entailed forms using natural logic. For example, "Blue cats play" produces ["Blue cats play", "cats play"]. This applies to all fragments, including those from clause splitting.

Confidence Threshold (min_confidence) Filters triplets below the specified confidence score (0.0 to 1.0). Higher values give fewer but higher-quality results.

# Fast extraction without variations
extractor = OpenIEExtractor(
    enable_clause_split=False,
    enable_entailment=False
)

# High-precision extraction
extractor = OpenIEExtractor(
    min_confidence=0.7
)

Attribution Metadata: Asserter Chains

Content embedded under attitude/speech verbs is reported, not asserted by the document author — "Tom said Sarah claimed the food was cold" does not assert that the food was cold. Each triplet carries an asserter_chain recording who asserts it, outermost asserter first; None means the author asserts it directly. The detection reuses Stanford OpenIE's own indirect-speech machinery (the same structure its clause splitter uses to refuse splitting reported clauses).

from triplet_extract import extract

for t in extract("Tom said Sarah claimed the chef burned the pasta."):
    print(f"({t.subject}, {t.relation}, {t.object})  asserted by: {t.asserter_chain}")

Output:

(Tom, said, Sarah claimed the chef burned the pasta)  asserted by: None
(Sarah, claimed, the chef burned the pasta)  asserted by: ['Tom']
(the chef, burned, the pasta)  asserted by: ['Tom', 'Sarah']
...

This is metadata only: rendered subject/relation/object strings are unchanged. Useful for knowledge graphs that track provenance, source reliability, and testimony.

Each triplet also carries asserter_links, the structured form of the chain: per link, the asserter, the governing verb lemma, the construction (ccomp/xcomp/quote), a speech_act flag (the canonical reported-speech verbs), and a negated flag — so a consumer can tell endorsement from denial (Tom denied X and Tom did not say X are not Tom asserting X). Chains are recorded for any complement-taking verb, including evidential ones (The study shows X attributes X to the study).

Direct quoted speech is handled too: "The food was amazing," said Tom. attributes the quoted content to Tom rather than leaking it as an author-level fact or producing cross-boundary garbage.

Canonical Names and Appositive Promotion

A subject like "My friend Sarah" names its referent in the appositive. Two features expose that, both gated purely structurally on the parse (a PROPN attached to the subject head by appos/flat — a proper noun elsewhere in the span, like "the senator from Ohio", modifies the referent rather than naming it and never qualifies):

subject_canonical (metadata): the proper-noun span naming the subject's referent — "My friend Sarah""Sarah", "Her colleague Dr. Chen""Dr. Chen", compound names stay whole ("Mary Jane Watson"), None when the subject has no name. With cluster_sources=True, subject_cluster_canonical (and cluster_canonical on asserter links) carry the cluster-wide canonical — the shortest name span across the cluster's mentions — so a headless mention like "My friend" recovers "Sarah" through its cluster.

Appositive promotion (renderings): the bare-name variant is emitted alongside the originals. Entailment shortening deletes dependent subtrees, so it can drop the name and keep "My friend" but never promote the name itself — leaving the most identity-bearing rendering missing. A non-restrictive appositive names the same referent, so promotion is an equivalence (Stanford's own verb patterns substitute appositives for their heads in object position; this extends the device to subjects):

for t in extract("My friend Sarah said the food was cold."):
    print(f"({t.subject}, {t.relation}, {t.object})")
# (My friend Sarah, said, the food was cold)
# (Sarah, said, the food was cold)        <- promoted
# (My friend, said, food was cold)
# ...

Promoted renderings inherit everything else unchanged — relation (including negation), object, confidence, and attribution metadata (asserter_chain survives promotion).

Pronoun Resolution (Opt-In)

resolve_coref=True substitutes third-person pronouns with their antecedents before extraction, using a port of the pronoun sieve from Stanford's deterministic coreference system (Lee et al., 2011) with one deliberate change: a pronoun is substituted ONLY when exactly one agreeing antecedent exists within the sieve's sentence window — otherwise it abstains and leaves the pronoun untouched. A wrong substitution poisons downstream facts; an unresolved pronoun is honest. Singular gendered pronouns ("he"/"she") resolve to PERSON antecedents; plural "they"/"them" resolve to a unique plural noun phrase ("I love these headphones. They sound amazing." → (these headphones, sound, amazing)).

extractor = OpenIEExtractor(resolve_coref=True)
triplets = extractor.extract_triplet_objects(
    "Obama is the president. Everyone says he has a nice smile."
)
# (Obama, has, a nice smile) — instead of (he, has, a nice smile)

Ambiguous cases abstain by design: with "Sarah met Mary at the cafe. She ordered coffee.", the pronoun stays "She" (two candidates the resolver cannot separate). Winograd-style pronouns ("it" with world-knowledge ambiguity) are out of scope and never substituted. Default is OFF because substitution changes rendered triplet strings.

Name gender (optional coref extra). English proper nouns carry no gender morphology, so by default a gendered pronoun whose referent is absent can resolve to the lone PERSON in scope regardless of that name's apparent gender ("Sarah arrived early. He was annoyed." → He→Sarah). Installing the coref extra adds a small CPU sentence-embedding model (BAAI/bge-small-en) that supplies a name-gender signal; a confident name/pronoun conflict then vetoes the resolution (He stays unresolved), while genuinely unisex names abstain and fall back to the uniqueness gate.

pip install triplet-extract[coref]

The cost is modest: the model is lazy-loaded only when a gender decision is needed (one ~2.6s load), and each distinct name is encoded once and cached, so the per-document overhead is negligible next to the coreference re-parse itself. The prior is auto-enabled when the extra is installed; coref_gender_prior=False disables it, and an application that already runs a sentence-embedding model can avoid a second copy by sharing it:

from triplet_extract import OpenIEExtractor
from triplet_extract.gender import NameGenderPrior

prior = NameGenderPrior(encoder=lambda strs: my_model.encode(strs, normalize_embeddings=True))
extractor = OpenIEExtractor(resolve_coref=True, coref_gender_prior=prior)

Source-Identity Clustering (Opt-In)

cluster_sources=True assigns a subject_cluster id to each triplet (and a cluster to each asserter link) so that different mentions of one source share an id — "My friend Sarah", "my friend", and "Sarah" become one cluster. It ports the lexicon-free structural sieves of Stanford's deterministic coreference (exact match, relaxed head match, shared proper name, head match with a conflicting-name guard). Metadata only — rendered strings are unchanged.

Batch Processing

For processing multiple texts efficiently:

texts = [
    "First sentence to process.",
    "Second sentence to process.",
    "Third sentence to process."
]

# GPU-accelerated if available, CPU fallback otherwise
results = extractor.extract_batch(texts, progress=True)

for text, triplets in zip(texts, results):
    print(f"\n{text}")
    print(f"  {len(triplets)} triplets extracted")

The system automatically uses GPU acceleration if triplet-extract[deepsearch] is installed and a CUDA GPU is available. Otherwise, it falls back to CPU with identical extraction quality.

Performance Tips

Reuse extractor instances when processing multiple texts:

# Good: Reuse the same extractor
extractor = OpenIEExtractor(min_confidence=0.5)
for text in texts:
    triplets = extractor.extract_triplet_objects(text)

# Avoid: Creates new extractor (reloads models) each time
for text in texts:
    triplets = extract(text, min_confidence=0.5)

Use batch processing for best performance:

results = extractor.extract_batch(texts, batch_size=32)

Verbose Logging

The library is silent by default. Enable logging to see internal operations:

import logging

logging.basicConfig(level=logging.DEBUG)  # Show all details
# or
logging.basicConfig(level=logging.INFO)   # Show major steps

from triplet_extract import extract
triplets = extract("Your text here")

How It Works

The system implements the three-stage pipeline from the Stanford OpenIE paper:

Stage 1: Clause Splitting Uses a pre-trained linear classifier to break complex sentences into independent clauses. The classifier was trained on the LSOIE dataset and considers dependency parse structure to make splitting decisions.

Stage 2: Forward Entailment Applies natural logic deletion rules to generate shorter entailed forms. Uses prepositional phrase attachment affinities to determine which constituents can be safely deleted while preserving truth.

Stage 3: Pattern Matching Extracts (subject, relation, object) triplets from sentence fragments using dependency patterns. Handles various syntactic constructions including copular sentences, prepositional phrases, and clausal complements.

The trained models (clause splitting classifier and PP attachment affinities) are from the original Stanford implementation and are included in this package.

Implementation Notes

This implementation uses spaCy for dependency parsing instead of Stanford CoreNLP. While the algorithm and models are the same, the parsers may produce different dependency trees for the same sentence. Differences in tokenization, POS tagging, and dependency labels mean that extraction results won't be identical to the original Java implementation.

In practice, core extractions remain highly compatible with Stanford OpenIE, though edge cases may differ, particularly with unusual capitalization or complex grammatical constructions. If you require exact compatibility with Stanford OpenIE output, please use the original Java implementation.

Limitations

Contrastive negation in appositives can mis-scope. In "The animals were taken on the ark by Noah, not Moses." the "not" negates the agent ("not Moses"), but the parser can attach it so that renderings come out as (animals | were taken not on | ark) and (animals | were taken | Moses) — the negation lands on the predicate and the contrasted agent dangles as a bare object. The extraction pipeline guarantees that a parse-level negation is never silently dropped from a rendering (a triple whose text inverts the polarity of its source is suppressed outright, and entailment never deletes arguments inside a negation's scope), but it cannot detect negation the parser attached to the wrong constituent in the first place. Sentences using the "by X, not Y" contrast pattern are the risk zone.

spaCy's statistical POS tagger can commit to a compound-noun reading of an entire clause when every token in sequence admits a noun-compatible analysis. extract("Dogs chase cats.") returns no triplets because the parse is Dogs/ADJ chase/NOUN cats/NOUN — no verb anywhere, so no extraction pattern has a predicate to anchor on. The trigger is a noun/verb-ambiguous word in verb position with bare nominals on both sides; a bare-plural subject alone is not the decisive factor. Anything that breaks the compound reading anywhere in the clause flips the tagger to the clausal parse and extraction succeeds: a determiner on the object ("Dogs chase the cats."), an adverb beside the verb ("Dogs often chase cats.", "Cheetahs run faster than dogs."), a pronoun subject ("They chase cats."), unambiguous verb morphology ("Dogs chased cats."), or an unambiguous verb ("Birds eat seeds."). Conversely, a determiner on the subject alone does not reliably fix it ("The dogs chase cats." fails on en_core_web_sm), and neither does 3sg inflection when the object stays bare ("The dog chases cats." fails — "chases" re-reads as a plural noun, like "dog races"); "The dog chases the cat." works because of the object's determiner. Larger models shrink the failure surface without eliminating it: en_core_web_md resolves the determiner-bearing variants but still misparses fully bare "Dogs chase cats." and "Dogs hunt mice." (en_core_web_trf untested). Stanford CoreNLP's tagger resolves these cases correctly. This rarely impacts real-world usage — formal writing scatters determiners, adverbs, and inflection through clauses — but aphorism-style generic SVO ("X chase Y") is the risk zone, and such sentences fail silently, yielding zero triplets.

Citation

If you use this library in research, please cite both this implementation and the original Stanford OpenIE paper:

This implementation:

@software{malec2025tripletextract,
  title={triplet-extract: GPU-accelerated Python implementation of Stanford OpenIE},
  author={Malec, Adrian Lucas},
  year={2025},
  url={https://github.com/adlumal/triplet-extract}
}

Original Stanford OpenIE paper:

@inproceedings{angeli2015openie,
  title={Leveraging Linguistic Structure For Open Domain Information Extraction},
  author={Angeli, Gabor and Johnson Premkumar, Melvin Jose and Manning, Christopher D},
  booktitle={Proceedings of the 53rd Annual Meeting of the Association for Computational Linguistics (ACL 2015)},
  year={2015}
}

Reference: Angeli, Gabor, Melvin Jose Johnson Premkumar, and Christopher D. Manning. "Leveraging Linguistic Structure For Open Domain Information Extraction." Association for Computational Linguistics (ACL), 2015. Paper | Stanford OpenIE | CoreNLP Github

Contributing

Bug reports and feature requests are welcome. Please open an issue on GitHub if you encounter problems or have suggestions for improvements.

License

GPL-3.0-or-later

This is a derivative work of Stanford OpenIE, which is licensed under GPL-3.0. The trained models included in this package are from the original Stanford implementation and remain under their GPL-3.0 license.

See LICENSE for details.

Links

Related packages

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

triplet_extract-0.5.0.tar.gz (20.9 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

triplet_extract-0.5.0-py3-none-any.whl (20.9 MB view details)

Uploaded Python 3

File details

Details for the file triplet_extract-0.5.0.tar.gz.

File metadata

  • Download URL: triplet_extract-0.5.0.tar.gz
  • Upload date:
  • Size: 20.9 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for triplet_extract-0.5.0.tar.gz
Algorithm Hash digest
SHA256 b420ea66691fa65fb7849af4a7b2a59f605b7a590cf0267729e8f51bc4776153
MD5 90a8cff81e4d80c4326ae82d4475746d
BLAKE2b-256 054e706b5c4f394c50e638d00484fcbe1db2a1ce43e3424014a3df31d2f2d858

See more details on using hashes here.

Provenance

The following attestation bundles were made for triplet_extract-0.5.0.tar.gz:

Publisher: release.yml on adlumal/triplet-extract

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file triplet_extract-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: triplet_extract-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 20.9 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for triplet_extract-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a64bdd5bc737402f246e087f116a062f84570d00d2e3ea96e59d2560a0daf19b
MD5 c912bbda70492478d28c7b7d29450bd0
BLAKE2b-256 184106c3cd25adf2243f58387c4d9db748cb490f83f52aec1c97dc6c46ca2e57

See more details on using hashes here.

Provenance

The following attestation bundles were made for triplet_extract-0.5.0-py3-none-any.whl:

Publisher: release.yml on adlumal/triplet-extract

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page