Repository navigation
What gliner2 2.0.0 actually offers for entity attributes #362
Description
Activity
- added a parent issue
on Sep 22, 2026 Resolution
Full write-up, 754 lines with per-claim
file:lineand command output:unstructured2graph/docs/research/gliner2-attributes.mdon branchresearch/gliner2-attributes(commit0020973). Branch only, no PR, same convention as the #345 note.1. No attribute mechanism exists on the joint path — none, anywhere
EntitySpeccarries six fields, none a value (joint_ie/schema.py:22-32), andentity()is keyword-only with no**aliases— unlikerelation(), so the absence reads as deliberate rather than an oversight.JointSchemahas nofield/structure/classification/entity_attributes;compile_schemahard-codes"json_structures": []and"classifications": [](joint_ie/compiler.py:26-28);JointResult/JointEntityhave no slot to put a value in.Verified by forcing it: a structure appended directly into
compiled.model_schemaruns without error and the result still contains only['entities', 'relations']— silently dropped.A trap worth writing down:
JointSchema.from_dictignores any unknown top-level key without complaint. A config file carrying anattributes:block would load cleanly, validate, and do nothing.2. The legacy API is the mechanism — three of them — and using it costs typed relations
entity_attributes()+AttributeGroup— closed label sets bound to entity spansstructure().field()— the only mechanism that extracts an actual valueclassification()— document-level, entity-free
All three compose with entities and relations in a single
extract()call (verified: result keys['facts', 'entities', 'topic', 'relation_extraction']). But that call is the legacy runtime #345 retired: relations there compile to{"head": "", "tail": ""}and endpoints come back as span dicts.So the trade is stark: values in the same pass, or typed relations — not both. Keeping
JointSchemameans a second inference call per window (+90% wall clock, measured), and that second call has no entity ids — which resurrects exactly the span-matching apparatus #345 established we could delete.3. Three landing places, only one of which is a real value
mechanism what comes back attached to entity_attributes{"label", "confidence"}— no offsets, no text groundingco-located with the entity row, but legacy output has no ids structure().field()a real span: {"text": "25:50", "confidence": 0.9999, "start": 82, "end": 87}nothing — document-scoped structure(mode="natural", anchor=...)per-anchor instances the only per-entity construct Record mode's anchor spans match joint entity ids exactly by
(start, end)— verified(414,419) -> e10,(540,545) -> e14— so coupling the two paths is mechanically recoverable even though the library does not hand it to you.4. Constrained by construction; nothing enforced during decoding; no feasibility signal
Closed sets (
AttributeGroup.labels, fieldchoices=) are enforced by only ever scoring declared labels — there is noallow_edgeanalogue because there is no search. Cardinality isdtype("str"->spans[0],"list"-> all);cardinality=/exclusive=are inert outside record mode.validators=(regex) is a provable post-hoc filter: the same span returns at byte-identical confidence0.9882827401161194with and without a matching validator. There is nofeasibleanalogue — the legacy result is a plaindict, so #350's infeasibility question has no counterpart here.Two sharp edges:
- A single-label group can never abstain. It is a softmax argmax: three unrelated entities given a
planetgroup all came backMars. choices=hallucinates. It returned"alpha"at confidence 0.99 from a list whose members appear nowhere in the text.
The only abstention route is
multi_label=Trueplus a threshold. An unfillable field returnsnull; an all-unfillable structure is silently dropped entirely.5. Composes with per-window driving, is cheap — and conflicts across windows
Costs on a 572-char window, CPU, median of 5: joint baseline 157ms; +1 attribute group (3 labels) +12ms; +6-field structure +12ms; +18-field structure +88ms; joint-then-legacy as two calls 299ms (+90%). Candidate growth is linear, not quadratic — attribute labels become extra entity queries (5 -> 8), unlike the endpoint expansion #350 measured.
The real problem is structural, and it lands on #352: a structure is one instance per document, so every window competes to fill every field. Same text, same schema —
purchase_datecame back as bothMarch 14, 2025andlast Saturday;pair_countas both3 pairsandThree pairs;personal_bestflipped from26:34to25:50purely by changing window size 220 -> 400. Nothing in the output says which is right.6. No version bump exists, let alone is needed
gliner2==2.0.0is installed and is the latest on PyPI. "Span attributes" isSchema.entity_attributes— its own docstring says the labels are "decoded as span attributes" (inference/schema.py:331), andAttributeGroupis a public export (__init__.py:42). The "2.5" upstream markets is the checkpoint generation (fastino/gliner2.5-base-v1), not a library version, and we already run it — itsBoundaryHeadSettingshasenable_records=True,enable_relations=True,enable_abstention=True.Requires-Dist: transformers<5,>=4.38is still 2.0.0's pin, so adopting attributes needs no new dependency, extra, or version, and nothing touches the CVE-2026-1839 floor.Beyond the brief: #350's gap was its vocabulary, not the API
Declaring
Duration,Quantity,TimeWindow,Money,Dateas entity types and relating into them expresses all five value facts on the joint path — one pass, entity ids and #345's constraint enforcement intact,feasible=True:REL owns_count Person:'user' -> Quantity:'3 pairs' conf=0.985 REL paid Person:'user' -> Money:'$129.99' conf=0.964 REL personal_best Person:'user' -> Duration:'26:34' conf=0.971 REL works_shift Person:'Admon' -> TimeWindow:'Sunday shift' conf=0.879So "3 of 6 question types have no expressible answer" (#350) is a property of that prototype's vocabulary, not of the library. Carried to #361 as a third option alongside attributes and reified assertions.
Three caveats, all visible in that same output: 2 of 4 values are wrong (
personal_bestbound26:34, the previous PB —25:50was never extracted as aDurationat all;works_shiftbound'Sunday shift'while'8am-4pm'sat two tokens away, extracted at conf 0.998); relations triplicated (the same 3.3x mention duplication #350 measured); 280ms against a 124ms baseline. The structure path got 5/5 on the same text where this got 2/4 — a sample of one, and not a quality result.Relatedly, and worth keeping: in the same single legacy pass, the relation mechanism answered
personal_best = 26:34while the structure mechanism answered25:50. Same model, same text, two mechanisms, two different answers.Could not determine
- Accuracy of any of this. Everything is one 572-char window. The 5/5-vs-2/4 split must not be read as a quality result; that needs the eval over the session corpus.
- Whether record mode's field-to-anchor binding is controllable from the schema. It bound a value 460 chars away to the wrong anchor — gave a runner's personal best to Admon the physio, twice, and left
shiftnull.occurrence_policy("all"|"first"|"error_on_ambiguous"|"latent_all") exists and was not swept;error_on_ambiguousin particular may surface ambiguity rather than guess. This is the most consequential unknown here, because record mode is the only per-entity value construct — folded into Values, not links: do entity attributes belong in the model #361 as work its session does. - Whether attributes survive
extract_long(source says yes; not run, since we own windowing per Wire a constrained ontology and read the edges #350/Chunking and the durability of constraints #352). - The span (non-boundary) architecture, which carries a second, separate attribute implementation with an extra overlap-dedupe pass.
- Whether a value-shaped-entity-type vocabulary could ever be derived (The derivation contract for LlmRecommendationStrategy #353) rather than hand-written.
- Calibration: structure-field confidences cluster at 0.99+, attribute confidences spread 0.44-0.99. Different heads, not calibrated against each other — a single threshold across both would be unsound.
Part of #344
Question
Can the installed
gliner2==2.0.0attach values to an entity — and if so, through which API, in what output shape, and under what constraints?#350 found that 3 of 6 question types in its sample have no expressible answer as a relation: a personal-best time (
25:50), a count (3), a shift window (8am-4pm). #361 has to decide whether attributes enter the model, and it cannot decide that without knowing what the library can actually do. This is a fact-finding ticket, not a decision: same shape as #345, which is also the quality bar — per-claimfile:lineand real command output, against the source on disk and a live run offastino/gliner2.5-base-v1, never the upstream blog.To answer:
gliner2.joint_ie.schema.JointSchema.entity()takesname, description, threshold, candidate_threshold, max_candidates, allow_nested— no obvious field slot. Does one exist elsewhere onJointSchema, on the compiled schema, or via**aliases? What JointSchema actually enforces #345 investigated relations only.gliner2'sSchemaexposes structure/classification tasks alongside entities and relations. Are those the real mechanism for values, what do they return, and can they run in the same pass as joint entity+relation extraction — or does using them mean a second inference call per window, and losing the entity-id coupling What JointSchema actually enforces #345 verified for relations?_extract_sync's span-matching apparatus, which What JointSchema actually enforces #345 retired)?allow_edge-style) or after? Is there afeasible-equivalent signal when a declared attribute cannot be filled? Wire a constrained ontology and read the edges #350 established that prohibitive constraints never make a window infeasible; check whether attributes change that.extract_long_text)? Does declaring attributes multiply candidates the way permissive endpoints do — Wire a constrained ontology and read the edges #350 measured typed vs all-types endpoint expansion — and what does it cost per window in wall clock?gliner2_backend.py's module docstring).Deliverable: a research note at
unstructured2graph/docs/research/gliner2-attributes.mdon aresearch/gliner2-attributesbranch — branch only, no PR, same convention as the #345 note — linked back here, with anything inconclusive flagged as such rather than smoothed over.Environment: the only venv with
gliner2installed is.claude/worktrees/backend-comparison/.venv.Blocks #361.