Querying collections

I've been discussing some issues with Markus off-list related to API issues we're working through in defining the YourSports API using Hydra. One of them deals with how to query a collection.

For an example, I may have a schema:SportsTeam with a collection expressing interests on the team (such as fan, like, dislike, neutral). Some data may look like the following (sorry for the long setup):

</giants> a :SportsTeam;
  :name "SF Giants";
  :interest [
    a :UserLikes;
    :linkKind "fan";
    :performer </gregg>
  ], [
    a :UserLikes;
    :likKind "foe";
    :performer </markus>
  ] .

As expressions of interest on a particular team may be quite large, I want to separate these into a PagedCollection. As an interim solution to avoid breaking a specific "interest" property on SportsTeam, I'm using a separate collection property, and referencing it collection property from the SupportedProperty on interest. A part of the API description looks like the following:

ysapi: a hydra:ApiDocumentation;
  hydra:supportedClass :SportsTeam .

:SportsTeam a hydra:Class;
  hydra:supportedProperty [
    a hydra:SupportedProperty;
    hydra:description """
	The interest property does not appear directly in an entity description; interests are asserted in a Collection identified as the value of the interestCollection property.

	Entries are managed through the associated Collection.
     """@en;
    hydra:property :interest;
    hydra:collectionProperty :interestCollection;
    hydra:writeOnly true;
    hydra:required false
  ], [
    a hydra:SupportedProperty;
    hydra:description """
	Identifies a Collection containing member references to all UserLikes along with the interest relationships from the Thing to each member of that Collection.
    """@en;
    hydra:property :interestCollection;
    hydra:writeOnly true;
    hydra:required false
  ] .

:interestCollection a hydra:Link .

:InterestCollection a hydra:Class;
   hydra:supportedOperation [
     a hydra:CreateResourceOperation;
     hydra:description "Indicate if you are a fan, like, neutral, dislike or a foe of something."@en;
     hydra:expects :UserLikes;
     hydra:method "POST";
     hydra:returns :UserLikes;
     hydra:title "Express interest in something"@en
   ];
   hydra:supportedProperty [
     a hydra:SupportedProperty;
     hydra:multiple false;
     hydra:property hydra:member;
     hydra:required false
   ] .

As :interest is not defined as a hydra:Link, but :interestCollection is, an API client will know to retrieve the collection value to get an InterestCollection (defined using rangeIncludes). The two resources that might enable this are the following:

# Giants resource
</giants> a :SportsTeam;
  :name "SF Giants";
  :interestCollection </giants/interests> .

# InterestCollection resource
</giants/interests> a :InterestCollection, hydra:Collection;
  hydra:member </giants/interests/gregg>, </giants/interests/markus> .
</giants> :interest </giants/interests/gregg>, </giants/interests/markus> .

# Interest resources
</giants/interests/gregg> a :UserLikes; :likeKind "fan"; :performer </gregg> .

</giants/interests/markus> a :UserLikes :likeKind "foe"; :performer </markus> .

Now, for the question about how to query such a collection.

So, how might I go about finding just the fans of the Giants? Certainly, one way would be to create a different collection property for each variation of the :likeKind property value, and implement server-side logic to perform an appropriate query. However, given the combinatorics of this, it probably makes sense to defer this logic to the API definition. As I understand it, I could create a SupportedProperty in :InterestCollection which is associated with some property with a a domain of :InterestCollection which would allow me to specify such a query. For example:

:InterestCollection a hydra:Class;
   hydra:supportedProperty [
     a hydra:SupportedProperty;
     hydra:multiple false;
     hydra:property hydra:member;
     hydra:required false
   ], [
    a hydra:SupportedProperty;
    hydra:property :searchInterestCollection
  ], .

:searchInterestCollection a hydra:TemplatedLink;
  hydra:search [
    a hydra:IriTemplate;
    hydra:template "{?collection}?likeKind={?kind}";
    hydra:mapping [
      a hydra:IriTemplateMapping;
      hydra:variable "collection";
      hydra:property :interestCollection;
      hydra:required true
    ], [
      a hydra:IriTemplateMapping;
      hydra:variable "kind";
      hydra:property :likeKind
      hydra:required true
    ] .

The documentation's a bit scant on TemplatedLinks, but I think this says that :searchInterestCollection is a property of an :InterestCollection. To search the collection, you construct an IRI using the template  "{?collection}?likeKind={?kind}". The "collection" variable is bound to the property :interestCollection, although the value of that would be in SportsTeam, not the InterestCollection itself. Perhaps there's a standin for the IRI of the subject. The "kind" variable is bound to the property :likeKind, although that is a property of a :UserLikes (in YS, anyway). Anyway, that  property is defined with a range restriction being one of "fan", "foe", etc., so an API client might have some way of knowing what kind of value to place here.

Another way to look at search is that a Collection represents a Graph composed of the collection resource, and the referenced member resources. Performing a search, could be thought of as defining a SPARQL query against this graph, which returns a CONSTRUCT based on the members of that graph. So, the searchInterestCollection could be thought of as being like the following query:

CONSTRUCT ?member
FROM </giants/interests>
WHERE {
  </giants/interests> hydra:member ?member .
  ?member :likeKind ?kind
  FILTER ?kind = "fan"
}

If we think of it this way, then it might be nice to consider doing aggregations as well, for example:

SELECT ?kind, COUNT(?member) AS ?count
FROM </giants/interests>
WHERE {
  </giants/interests> hydra:member ?member .
  ?member :likeKind ?kind
}
GROUP BY ?kind

Gregg Kellogg
gregg@greggkellogg.net

Received on Saturday, 12 April 2014 21:50:12 UTC