Class AnnotationsWebService.AnnotationSearchResultValueObject

java.lang.Object
ubic.gemma.rest.AnnotationsWebService.AnnotationSearchResultValueObject
Enclosing class:
AnnotationsWebService

public static final class AnnotationsWebService.AnnotationSearchResultValueObject extends Object
Author:
tesarst
  • Constructor Details

    • AnnotationSearchResultValueObject

      public AnnotationSearchResultValueObject(String value, String valueUri, String category, String categoryUri, Integer usageCount, @Nullable String definition, @Nullable List<AnnotationsWebService.OntologyTermSimpleValueObject> parents, @Nullable String matchedVia, @Nullable String matchedText, @Nullable Long geneCount, @Nullable Map<String,Integer> priorCategories, @Nullable Long taxonId, @Nullable String taxonCommonName, @Nullable String taxonScientificName, @Nullable AnnotationsWebService.ExampleUsageValueObject exampleUsage, @Nullable Long priorCurationCount, @Nullable AnnotationsWebService.LexicalTermMetadataValueObject sourceMetadata, @Nullable AnnotationsWebService.TaxonConstraintValueObject taxonConstraint, @Nullable Boolean viaSubClassOf)
      Creates a new AnnotationSearchResultValueObject instance.
      Parameters:
      value -
      valueUri -
      category - The category a curator has already applied to this URI in the corpus — NOT what kind of thing the term is. It is carried over from the Characteristic rows the search matched, so a term nobody has tagged yet is null however obvious its kind: CLO's ahl-1 cell is as uncategorised as any Cellosaurus row, because neither has ever been used. That makes absent mean "unused", never "conflicts with the category you asked for" — a client that filters rows on category equality discards every term it would be the first to apply, which is most of a freshly loaded vocabulary. CAB lost two weeks to exactly that reading (2026-08-18): one previously-curated CLO row suppressed every other registration of the same cell line.

      priorCategories is the same signal unflattened, with the per-category counts, and is the better one to resolve on. For "what kind of thing is this", walk parents on an ontology term, or read sourceMetadata.cellLineType / strainType on a flat catalogue — those are declared by the source rather than inferred from curation.

      categoryUri - URI of category, on the same terms: the curated category's URI, null when unused.
      usageCount -
      definition - Definition of the term, if enriched. Null indicates "not enriched" (the typeahead caller can lazy-load via /annotations/term?uri=X); empty string indicates "enriched, no definition known". Populated for the top-25 search hits only — see getTerms.
      parents - Nearest is_a OR part_of parents of this term, if enriched. Null indicates "not enriched" (sentinel for lazy-load); empty list indicates "enriched, no parents" (e.g. top-level term). Populated for the top-25 search hits only — see getTerms.
      matchedVia - Which Lucene field most likely produced this hit: preferred_label, exact_synonym, narrow_synonym, related_synonym, broad_synonym, alt_label (all equality tiers), or label_prefix, label_tokens, synonym_tokens (neighbourhood tiers). Back-computed by replaying the query against the term's label and indexed synonyms — see computeMatchAttribution.

      Populated for the top-25 hits only. null there means UNATTRIBUTABLE, not "weak": the hit came via a field we do not probe (definition, obo_id) or a pure relevance rank. Label-level tiers are still filled in from the row's own label when the term itself could not be resolved, so a row whose label equals the query never reports null; but a SYNONYM-exact hit whose owning ontology is unloaded does, because without the term its synonyms are invisible. A client cannot therefore implement "equality tiers only" from this field alone — treat null as unknown and fall back to label equality.

      matchedText - The actual label or synonym text that scored the match — surfaced so the UI can render "↪ matches synonym 'Ammon's horn'" beneath the preferred label. Equals the term's label when matchedVia is preferred_label. Null when matchedVia is null.
      geneCount - Distinct genes annotated with this term (including the descendant terms walked under the request's geneCountMaxTerms cap). Populated only when the caller passed includeGeneCount=true; null otherwise. Counts include propagation through GO subClassOf descendants by default — set via Gene2GOAssociationService.countByGOTermUris.
      priorCategories - Distinct-experiment counts grouped by the category that prior curators applied when tagging this URI on an experiment — e.g. {"cell line": 14, "protein": 1}. Lets resolvers break ambiguous label hits by curated history: a URI tagged 14× as a cell line and 1× as a protein is almost certainly a cell line, regardless of which ontology label the query happened to match. Populated for the top-N kept hits only; null on synthetic gene-fanout rows and on responses where the lookup was skipped (e.g. /annotations/parents). An empty map means "the URI has been used in curation but never with a non-null category" (rare; carries no signal).
      taxonId - Taxon (species) of a gene hit — the gene's Taxon.id. Null on ontology-term hits (taxon is a gene-only attribute). Lets a curator tell which species a returned gene belongs to without resolving the NCBI Gene URI against an external database.
      taxonCommonName - Common name of a gene hit's taxon (e.g. "mouse"). Null on ontology-term hits.
      taxonScientificName - Scientific name of a gene hit's taxon (e.g. "Mus musculus"), to disambiguate when the common name is ambiguous. Null on ontology-term hits.
      exampleUsage - One representative, ACL-visible example of how this term has actually been used in the corpus — so a curator picking an unfamiliar/rare term can see it in context (e.g. "wild type" under the "genotype" factor of an accessible dataset). Populated only when the caller passes includeExampleUsage=true; null when the flag is off, on synthetic gene rows, or when the term has no accessible usage. Gate rendering client-side (e.g. only for low usageCount).
      priorCurationCount - Distinct experiments on which a prior curator annotated this term after being given the query string itself — the evidence ?rank=commonality orders by.

      Surfaced so the ordering can be audited rather than trusted. A hit promoted on n=1 and one promoted on n=508 are the same position in the list and very different claims, and only this field tells them apart. The ranker orders and never filters, so a consumer that disagrees with the corpus can always recover the term it wanted; that recovery is only possible if the strength of the evidence is visible.

      Populated when the per-string prior was computed for the request (i.e. under rank=commonality); null otherwise, and null rather than 0 on rows the prior did not cover. 0 means the string has been written in the corpus but never for this term — a real and useful negative.

      ⚠️ Curation history, so it carries curation's mistakes. See AnnotationsWebService.PriorCurationValueObject for the fuller warning and for the agreement figure that shows whether curators were actually consistent.

      sourceMetadata - Descriptive metadata for hits that came from a flat lexical source (Cellosaurus cell lines, MGI mouse strains): species, cell-line type, donor sex, strain type, and any problematic-entry flag. Null for hits from a real ontology, which carry none of this.

      A cell-line name on its own is not actionable — it does not say which organism the line came from, and it does not say the line is a known misidentified one. Both facts exist in the source and used to be dropped at parse time. This is descriptive metadata about the term, like definition; it is NOT a value to annotate an experiment with.

      🛑 Gemma does not filter these vocabularies by species — see AnnotationsWebService.LexicalTermMetadataValueObject. Scoping is the caller's decision, made with ncbiTaxonId in hand.

      taxonConstraint - The taxon this ONTOLOGY TERM is restricted to, when the ontology declares one — OBO's in_taxon. Null for the overwhelming majority of terms, which declare no constraint.

      🛑 This is NOT taxonId and the two must not be conflated. taxonId says "this hit IS a gene belonging to that species"; this says "this term only APPLIES to that species". Different claims about different things, which is why it is a separate field rather than a second meaning loaded onto an existing one.

      Why it matters: MONDO declares in_taxon on 3,201 terms and only 30 of them are human, so it is overwhelmingly a marker for "this term is not about your organism". Without it, MONDO:0700199 sheep lung adenocarcinoma is indistinguishable from a mouse disease — right namespace, right organ, wrong species — and the category→namespace table structurally cannot catch that class, because the namespace is correct. A tag of exactly that shape reached a C57BL/6 mouse experiment on 2026-08-15.

      Populated for the top-N enriched hits only, like definition. Gemma does not act on it: whether a species mismatch is a reject or a repair is the caller's decision, and MONDO's crossSpeciesExactMatch often supplies the counterpart to repair TO.

      viaSubClassOf - How this term was reached from the one that was asked about, on /annotations/parents and /annotations/children only. true = by rdfs:subClassOf alone; false = only by crossing one of the non-taxonomic relations Gemma follows; null = not computed (every other route that emits this object).

      Both kinds of edge are walked and the flattened list cannot be partitioned by the caller. On CHEBI the non-taxonomic set is the part_of family plus has_role (added in ChebiOntologyService), and CHEBI asserts a compound's roles with has_role, so on a CHEBI term this field is the structure/role split. Measured on gemma2, 2026-09-04, vancomycin with direct=true — 4 parents, one subClassOf and three role fillers:

      glycopeptide         CHEBI_24396   viaSubClassOf: true
      antibacterial drug   CHEBI_36047   viaSubClassOf: false
      antimicrobial agent  CHEBI_33281   viaSubClassOf: false
      bacterial metabolite CHEBI_76969   viaSubClassOf: false
      

      The alternative is to ask each parent for its own ancestors and test for a CHEBI role root, which costs one extra request per parent and has to pick the right root: testing role (CHEBI_50906) over-keeps, because glycopeptide, peptide and organic amino compound all carry it through chemical role; testing biological role (CHEBI_24432) alone drops antiinfective agent, which sits under application (CHEBI_33232). This field needs no root and no second request.

      It says which edge was crossed, never whether the term is worth keeping. role, biological role and application are as uninformative as chemical entity and are all reached across has_role.

  • Method Details

    • getValue

      public String getValue()
    • getValueUri

      public String getValueUri()
    • getCategory

      public String getCategory()
      The category a curator has already applied to this URI in the corpus — NOT what kind of thing the term is. It is carried over from the Characteristic rows the search matched, so a term nobody has tagged yet is null however obvious its kind: CLO's ahl-1 cell is as uncategorised as any Cellosaurus row, because neither has ever been used. That makes absent mean "unused", never "conflicts with the category you asked for" — a client that filters rows on category equality discards every term it would be the first to apply, which is most of a freshly loaded vocabulary. CAB lost two weeks to exactly that reading (2026-08-18): one previously-curated CLO row suppressed every other registration of the same cell line.

      priorCategories is the same signal unflattened, with the per-category counts, and is the better one to resolve on. For "what kind of thing is this", walk parents on an ontology term, or read sourceMetadata.cellLineType / strainType on a flat catalogue — those are declared by the source rather than inferred from curation.

    • getCategoryUri

      public String getCategoryUri()
      URI of category, on the same terms: the curated category's URI, null when unused.
    • getUsageCount

      public Integer getUsageCount()
    • getDefinition

      @Nullable public String getDefinition()
      Definition of the term, if enriched. Null indicates "not enriched" (the typeahead caller can lazy-load via /annotations/term?uri=X); empty string indicates "enriched, no definition known". Populated for the top-25 search hits only — see getTerms.
    • getParents

      Nearest is_a OR part_of parents of this term, if enriched. Null indicates "not enriched" (sentinel for lazy-load); empty list indicates "enriched, no parents" (e.g. top-level term). Populated for the top-25 search hits only — see getTerms.
    • getMatchedVia

      @Nullable public String getMatchedVia()
      Which Lucene field most likely produced this hit: preferred_label, exact_synonym, narrow_synonym, related_synonym, broad_synonym, alt_label (all equality tiers), or label_prefix, label_tokens, synonym_tokens (neighbourhood tiers). Back-computed by replaying the query against the term's label and indexed synonyms — see computeMatchAttribution.

      Populated for the top-25 hits only. null there means UNATTRIBUTABLE, not "weak": the hit came via a field we do not probe (definition, obo_id) or a pure relevance rank. Label-level tiers are still filled in from the row's own label when the term itself could not be resolved, so a row whose label equals the query never reports null; but a SYNONYM-exact hit whose owning ontology is unloaded does, because without the term its synonyms are invisible. A client cannot therefore implement "equality tiers only" from this field alone — treat null as unknown and fall back to label equality.

    • getMatchedText

      @Nullable public String getMatchedText()
      The actual label or synonym text that scored the match — surfaced so the UI can render "↪ matches synonym 'Ammon's horn'" beneath the preferred label. Equals the term's label when matchedVia is preferred_label. Null when matchedVia is null.
    • getGeneCount

      @Nullable public Long getGeneCount()
      Distinct genes annotated with this term (including the descendant terms walked under the request's geneCountMaxTerms cap). Populated only when the caller passed includeGeneCount=true; null otherwise. Counts include propagation through GO subClassOf descendants by default — set via Gene2GOAssociationService.countByGOTermUris.
    • getPriorCategories

      @Nullable public Map<String,Integer> getPriorCategories()
      Distinct-experiment counts grouped by the category that prior curators applied when tagging this URI on an experiment — e.g. {"cell line": 14, "protein": 1}. Lets resolvers break ambiguous label hits by curated history: a URI tagged 14× as a cell line and 1× as a protein is almost certainly a cell line, regardless of which ontology label the query happened to match. Populated for the top-N kept hits only; null on synthetic gene-fanout rows and on responses where the lookup was skipped (e.g. /annotations/parents). An empty map means "the URI has been used in curation but never with a non-null category" (rare; carries no signal).
    • getTaxonId

      @Nullable public Long getTaxonId()
      Taxon (species) of a gene hit — the gene's Taxon.id. Null on ontology-term hits (taxon is a gene-only attribute). Lets a curator tell which species a returned gene belongs to without resolving the NCBI Gene URI against an external database.
    • getTaxonCommonName

      @Nullable public String getTaxonCommonName()
      Common name of a gene hit's taxon (e.g. "mouse"). Null on ontology-term hits.
    • getTaxonScientificName

      @Nullable public String getTaxonScientificName()
      Scientific name of a gene hit's taxon (e.g. "Mus musculus"), to disambiguate when the common name is ambiguous. Null on ontology-term hits.
    • getExampleUsage

      @Nullable public AnnotationsWebService.ExampleUsageValueObject getExampleUsage()
      One representative, ACL-visible example of how this term has actually been used in the corpus — so a curator picking an unfamiliar/rare term can see it in context (e.g. "wild type" under the "genotype" factor of an accessible dataset). Populated only when the caller passes includeExampleUsage=true; null when the flag is off, on synthetic gene rows, or when the term has no accessible usage. Gate rendering client-side (e.g. only for low usageCount).
    • getPriorCurationCount

      @Nullable public Long getPriorCurationCount()
      Distinct experiments on which a prior curator annotated this term after being given the query string itself — the evidence ?rank=commonality orders by.

      Surfaced so the ordering can be audited rather than trusted. A hit promoted on n=1 and one promoted on n=508 are the same position in the list and very different claims, and only this field tells them apart. The ranker orders and never filters, so a consumer that disagrees with the corpus can always recover the term it wanted; that recovery is only possible if the strength of the evidence is visible.

      Populated when the per-string prior was computed for the request (i.e. under rank=commonality); null otherwise, and null rather than 0 on rows the prior did not cover. 0 means the string has been written in the corpus but never for this term — a real and useful negative.

      ⚠️ Curation history, so it carries curation's mistakes. See AnnotationsWebService.PriorCurationValueObject for the fuller warning and for the agreement figure that shows whether curators were actually consistent.

    • getSourceMetadata

      @Nullable public AnnotationsWebService.LexicalTermMetadataValueObject getSourceMetadata()
      Descriptive metadata for hits that came from a flat lexical source (Cellosaurus cell lines, MGI mouse strains): species, cell-line type, donor sex, strain type, and any problematic-entry flag. Null for hits from a real ontology, which carry none of this.

      A cell-line name on its own is not actionable — it does not say which organism the line came from, and it does not say the line is a known misidentified one. Both facts exist in the source and used to be dropped at parse time. This is descriptive metadata about the term, like definition; it is NOT a value to annotate an experiment with.

      🛑 Gemma does not filter these vocabularies by species — see AnnotationsWebService.LexicalTermMetadataValueObject. Scoping is the caller's decision, made with ncbiTaxonId in hand.

    • getTaxonConstraint

      @Nullable public AnnotationsWebService.TaxonConstraintValueObject getTaxonConstraint()
      The taxon this ONTOLOGY TERM is restricted to, when the ontology declares one — OBO's in_taxon. Null for the overwhelming majority of terms, which declare no constraint.

      🛑 This is NOT taxonId and the two must not be conflated. taxonId says "this hit IS a gene belonging to that species"; this says "this term only APPLIES to that species". Different claims about different things, which is why it is a separate field rather than a second meaning loaded onto an existing one.

      Why it matters: MONDO declares in_taxon on 3,201 terms and only 30 of them are human, so it is overwhelmingly a marker for "this term is not about your organism". Without it, MONDO:0700199 sheep lung adenocarcinoma is indistinguishable from a mouse disease — right namespace, right organ, wrong species — and the category→namespace table structurally cannot catch that class, because the namespace is correct. A tag of exactly that shape reached a C57BL/6 mouse experiment on 2026-08-15.

      Populated for the top-N enriched hits only, like definition. Gemma does not act on it: whether a species mismatch is a reject or a repair is the caller's decision, and MONDO's crossSpeciesExactMatch often supplies the counterpart to repair TO.

    • getViaSubClassOf

      @Nullable public Boolean getViaSubClassOf()
      How this term was reached from the one that was asked about, on /annotations/parents and /annotations/children only. true = by rdfs:subClassOf alone; false = only by crossing one of the non-taxonomic relations Gemma follows; null = not computed (every other route that emits this object).

      Both kinds of edge are walked and the flattened list cannot be partitioned by the caller. On CHEBI the non-taxonomic set is the part_of family plus has_role (added in ChebiOntologyService), and CHEBI asserts a compound's roles with has_role, so on a CHEBI term this field is the structure/role split. Measured on gemma2, 2026-09-04, vancomycin with direct=true — 4 parents, one subClassOf and three role fillers:

      glycopeptide         CHEBI_24396   viaSubClassOf: true
      antibacterial drug   CHEBI_36047   viaSubClassOf: false
      antimicrobial agent  CHEBI_33281   viaSubClassOf: false
      bacterial metabolite CHEBI_76969   viaSubClassOf: false
      

      The alternative is to ask each parent for its own ancestors and test for a CHEBI role root, which costs one extra request per parent and has to pick the right root: testing role (CHEBI_50906) over-keeps, because glycopeptide, peptide and organic amino compound all carry it through chemical role; testing biological role (CHEBI_24432) alone drops antiinfective agent, which sits under application (CHEBI_33232). This field needs no root and no second request.

      It says which edge was crossed, never whether the term is worth keeping. role, biological role and application are as uninformative as chemical entity and are all reached across has_role.

    • equals

      public boolean equals(Object o)
      Overrides:
      equals in class Object
    • hashCode

      public int hashCode()
      Overrides:
      hashCode in class Object
    • toString

      public String toString()
      Overrides:
      toString in class Object