You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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.
Which fields participate. Every filterablereference 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.
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.
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.
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.
Context
SearchQuery.whereis a list ofFilterclauses, 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 GraphQLwhereinput mirrors that one-to-one – one input field perfilterablefield.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 correcttotal, 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_bysupports||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:
IR shape. A new
Filtervariant rather than an overload of{ field, in }– the existing variants are discriminated by operator and validated per field kind, andfield: '*'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.Which fields participate. Every
filterablereferencefield 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.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,contentLocationat once. So when computing thecreatorfacet, does the cross-field clause drop entirely, drop only itscreatordisjunct, or stay whole? Each gives different counts and each is defensible; this needs deciding before it ships, not after.GraphQL surface.
<Type>Wherehas 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.Port contract.
assertValidQuerymust 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
total, ranking and facet counts.assertValidQueryrejects a cross-field clause naming unknown, non-filterable or non-reference fields.Notes
*.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.whereinput (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.