Entity Extraction: Domain Schemas
How to use pre-built domain schemas and define custom entity types for domain-specific extraction.
|
Domain schemas improve extraction quality for specialized text: the entity-type descriptions are what the model reads, so a medical schema that describes |
Available Built-In Schemas
Eight schemas ship with the library. list_schemas() is the authoritative list; get_schema(name) returns the DomainSchema (entity types plus their descriptions).
from neo4j_agent_memory.extraction import get_schema, list_schemas
print(list_schemas())
# ['poleo', 'podcast', 'news', 'scientific', 'business', 'entertainment', 'medical', 'legal']
print(list(get_schema("medical").entity_types))
# ['disease', 'drug', 'symptom', 'procedure', 'body_part', 'gene', 'organism']
| Schema | Optimized For | Key Entity Types |
|---|---|---|
|
General investigations and intelligence |
person, organization, location, event, object |
|
Podcast and interview transcripts |
person, company, product, concept, book, technology |
|
News articles and journalism |
person, organization, location, event, date |
|
Research papers |
author, institution, method, dataset, metric, tool |
|
Earnings calls, market analysis, deal announcements |
company, person, product, industry, financial_metric |
|
Film and television coverage |
actor, director, film, tv_show, character, award |
|
Healthcare text |
disease, drug, symptom, procedure, body_part, gene |
|
Legal documents |
case, person, organization, law, court, monetary_amount |
Extracted entities come back mapped onto POLE+O types with a subtype, so drug becomes OBJECT / DRUG and actor becomes PERSON / ACTOR.
from neo4j_agent_memory.extraction import GLiNEREntityExtractor
extractor = GLiNEREntityExtractor.for_schema("business", threshold=0.4)
result = await extractor.extract(
"CEO Sarah Mitchell said TechVision Cloud revenue reached $2.4 billion, up 34%."
)
for entity in result.filter_invalid_entities().entities:
print(f"{entity.name}: {entity.full_type}")
# Sarah Mitchell: PERSON
# TechVision Cloud: OBJECT:PRODUCT
# $2.4 billion: OBJECT:FINANCIAL_METRIC
|
Runnable version: |
Custom Schema: Financial Services
There is no built-in financial schema — finance is a good example of a domain you define yourself with DomainSchema. The descriptions are the prompt, so write them for the model.
from neo4j_agent_memory.extraction import DomainSchema, GLiNEREntityExtractor
financial_schema = DomainSchema(
name="financial",
entity_types={
"person": "A client, advisor or contact, such as 'Sarah Johnson'",
"organization": "A company, fund or institution holding or managing assets",
"security": "A stock, bond, ETF or fund, such as 'Treasury Bond'",
"ticker": "A stock or fund symbol, such as 'AAPL' or 'BND'",
"account": "An account type or number, such as 'IRA' or 'Account #12345'",
"amount": "A dollar amount, percentage or share count",
"date": "A date or time period, such as 'Q4 2024'",
"sector": "An industry sector, such as 'Technology'",
"risk_profile": "A risk classification, such as 'moderate-growth'",
},
)
extractor = GLiNEREntityExtractor(schema=financial_schema, threshold=0.4)
text = """
Client meeting with Acme Investment Holdings regarding their Q4 portfolio review.
They currently hold 10,000 shares of Northwind Industries (NWI) and 5,000 shares
of Orion Systems (ORN). Risk tolerance remains moderate-growth. Advisor Sarah
Johnson recommended a 15% allocation to technology, balanced with fixed income.
"""
result = await extractor.extract(text)
for entity in result.filter_invalid_entities().entities:
print(f"{entity.name}: {entity.full_type}")
Unknown labels map to OBJECT with the label as the subtype (security becomes OBJECT:SECURITY), so custom schemas need no library change to be queryable.
Custom Schema: Ecommerce Retail
ecommerce_schema = DomainSchema(
name="ecommerce",
entity_types={
"customer": "A customer name or customer ID",
"product": "A product name, such as 'running shoe, model X'",
"sku": "A product identifier, such as 'NKE-AM90-001'",
"brand": "A brand name",
"category": "A product category, such as 'Footwear'",
"order_id": "An order identifier, such as 'ORD-98765'",
"carrier": "A shipping carrier",
"location": "An address, store or warehouse",
"payment_method": "A payment type, such as a wallet or card brand",
"promotion": "A coupon or discount, such as '20% off'",
},
)
extractor = GLiNEREntityExtractor(schema=ecommerce_schema, threshold=0.4)
text = """
Customer inquiry from Jane Doe (Gold member) about order #ORD-98765.
She ordered the Trailrunner 90 in size 9 (SKU: TRL-90-WHT-9) from our
mobile app. The package shipped via Northbound Freight to Brooklyn, NY.
"""
result = await extractor.extract(text)
for entity in result.filter_invalid_entities().entities:
print(f"{entity.name}: {entity.full_type}")
Custom Domain Schemas
Define Entity Types
A GLiNER extractor takes a DomainSchema: a name plus a {label: description} mapping. Labels are lowercase (GLiNER prefers them that way) and the description is the prompt.
from neo4j_agent_memory.extraction import DomainSchema, GLiNEREntityExtractor
insurance_schema = DomainSchema(
name="insurance",
entity_types={
"policyholder": "An insurance policy holder or applicant, a person or a company",
"policy": "An insurance policy with a number, such as 'Policy #INS-2024-001'",
"claim": "An insurance claim reference, such as 'Claim #CLM-98765'",
"coverage": "A type of insurance coverage, such as 'liability' or 'collision'",
"premium": "An insurance premium amount, such as '$500/month'",
"vehicle": "An insured vehicle, such as '2024 sedan, model X'",
},
)
extractor = GLiNEREntityExtractor(schema=insurance_schema, threshold=0.4)
|
Write descriptions as prompts. The description is what the model reads to decide whether a span matches this type. "An insurance policy holder or applicant" is better than "a person" — it gives the model the domain context it needs to avoid false positives. Naming a representative value inside the description ("such as 'Policy #INS-2024-001'") helps more than a longer abstract definition. |
Extend Built-In Schemas
DomainSchema.entity_types is a plain dict, so extending a built-in schema is a merge:
from neo4j_agent_memory.extraction import DomainSchema, GLiNEREntityExtractor, get_schema
base_schema = get_schema("business")
extended_schema = DomainSchema(
name="business_extended",
entity_types={
**base_schema.entity_types,
"loyalty_tier": "A customer loyalty program tier, such as 'Gold member'",
"subscription": "A subscription service or plan",
},
)
extractor = GLiNEREntityExtractor(schema=extended_schema, threshold=0.4)
Persist Schemas to Neo4j
SchemaManager stores EntitySchemaConfig — the POLE+O type registry used for validation and for MemorySettings.schema_config, which is a different object from the GLiNER DomainSchema above. Persist one, then derive the extractor schema from it:
from neo4j_agent_memory.extraction import DomainSchema, GLiNEREntityExtractor
from neo4j_agent_memory.schema import EntitySchemaConfig, EntityTypeConfig, SchemaManager
insurance_types = EntitySchemaConfig(
name="insurance",
version="1.0",
description="Schema for insurance industry context graphs",
entity_types=[
EntityTypeConfig(
name="POLICYHOLDER",
description="An insurance policy holder or applicant",
),
EntityTypeConfig(
name="POLICY",
description="An insurance policy with a number",
),
],
)
# ``client`` is a connected MemoryClient; ``client.graph`` is its Neo4jClient.
manager = SchemaManager(client.graph)
stored = await manager.save_schema(insurance_types, created_by="admin", set_active=True)
print(f"Schema saved with ID: {stored.id}")
# Load it in another session and build the extractor from it.
loaded = await manager.load_schema("insurance")
assert loaded is not None
extractor = GLiNEREntityExtractor(
schema=DomainSchema(
name=loaded.name,
entity_types={
entity_type.name.lower(): entity_type.description or entity_type.name
for entity_type in loaded.entity_types
},
),
threshold=0.4,
)
Relationship Extraction
GLiREL (No LLM Required)
GLiREL is an optional dependency (pip install glirel) — check for it before constructing the combined extractor.
from neo4j_agent_memory.extraction import GLiNERWithRelationsExtractor, is_glirel_available
if is_glirel_available():
extractor = GLiNERWithRelationsExtractor.for_schema("business", entity_threshold=0.4)
text = """
Jane Doe purchased a Trailrunner 90 from our Manhattan store.
The product was manufactured by Northwind Industries.
"""
result = await extractor.extract(text)
for rel in result.relations:
print(f" ({rel.source}) -[:{rel.relation_type}]-> ({rel.target})")
# (Jane Doe) -[:PURCHASED]-> (Trailrunner 90)
# (Trailrunner 90) -[:MANUFACTURED_BY]-> (Northwind Industries)
Custom Relationship Types
relation_types is a {name: description} mapping, the same shape as a schema’s entity types:
financial_relations = {
"ADVISES": "A financial advisor advises a client",
"HOLDS": "An account holds a security position",
"TRADED": "An executed trade in a security",
"SUBSIDIARY_OF": "A company is a subsidiary of a parent company",
"CUSTODIED_AT": "Assets are custodied at an institution",
}
extractor = GLiNERWithRelationsExtractor.for_schema(
"business",
entity_threshold=0.4,
relation_threshold=0.4,
relation_types=financial_relations,
)