Skip to content

Filter by IRI across all reference fields (cross-field "related to" filter) #611

Description

@ddeboer

Context

SearchQuery.where is a list of Filter clauses, each naming exactly one declared field: { field, in } for keyword/reference, { field, range } for the numeric/date kinds, { field, is } for boolean. The semantics are AND across fields, OR within a field, and the GraphQL where input mirrors that one-to-one – one input field per filterable field.

There is no way to express OR across fields.

Problem

The question “show me everything related to this entity” has no answer in the current model. An IRI can appear in several reference fields of the same type – creator, about, contentLocation, locationCreated, genre – and a consumer that wants every document referencing a person, place or term must issue one query per reference field and merge the results client-side, losing a correct total, correct ranking and correct facet counts in the process.

This is the natural query behind an entity landing page (“heritage objects related to Vincent van Gogh”), and it is exactly the case NDE’s valeros API draft reserves syntax for: alongside creator.id:<iri> it defines *.id:<iri> – “by ID for all properties”.

Engine-side this is cheap: Typesense filter_by supports || across fields, so a cross-field reference filter compiles to one disjunction in the same query. The work is in the model and the contract.

Proposal

Add a cross-field reference filter to the query IR and the surface. Open questions worth settling first, roughly in dependency order:

  1. IR shape. A new Filter variant rather than an overload of { field, in } – the existing variants are discriminated by operator and validated per field kind, and field: '*' would be a stringly special case that every consumer has to remember to handle. Something closer to { fields: readonly string[], in: readonly string[] } keeps it typed and lets the surface decide the field set.

  2. Which fields participate. Every filterable reference field is the obvious default. The alternative – an explicit declaration-level list, i.e. the deployment names the fields that make up “related to” – is more control but another declaration concept. Default first, unless DR or object search needs the narrower set.

  3. Facet semantics – the subtle part. Skip-own-filter (ADR 0005, Cut the faceted page to a single engine round-trip (overlap + ride the listing search) #556) removes a facet’s own clause when computing its buckets. A cross-field filter has no single owning field: it touches creator, about, contentLocation at once. So when computing the creator facet, does the cross-field clause drop entirely, drop only its creator disjunct, or stay whole? Each gives different counts and each is defensible; this needs deciding before it ships, not after.

  4. GraphQL surface. <Type>Where has one input field per filterable field, so a cross-field filter needs either a reserved input field (ThingWhere { …, anyReference: StringFilter }) or a separate root argument (relatedTo: [String!]). Reserved names collide with declared field names – worth checking the declaration validation rejects a field named whatever we reserve.

  5. Port contract. assertValidQuery must validate the new variant (fields exist, are filterable, are references), so a second adapter (Add an OpenSearch engine adapter for large-scale object search #572) cannot quietly ignore it.

Acceptance

  • One query answers “documents referencing this IRI in any reference field”, with a correct total, ranking and facet counts.
  • The cross-field clause compiles to a single engine round-trip, not one per field.
  • Facet semantics under a cross-field filter are decided, documented in the ADR, and covered by tests.
  • assertValidQuery rejects a cross-field clause naming unknown, non-filterable or non-reference fields.
  • The SDL snapshot pins whatever the surface shape turns out to be.

Notes

  • Surfaced while comparing our surface with the valeros API draft (above), whose *.id: parameter is the same capability expressed in a REST idiom. That draft is otherwise a resource API asking questions this family has already answered; this and hit highlighting (Support hit highlighting across the search family #610) are the two places it is ahead of us.
  • Pre-1.0 contract point: this adds to the frozen where input (ADR 0004), so the shape is cheaper to settle before v1 than after – though unlike Support hit highlighting across the search family #610 an additive input field is a softer break.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions