Search code examples
sparqlrdfsemantic-webowlontology

UNION case containing FILTER NOT EXISTS and OPTIONAL doesn't produce results?


I have a query that selects some items. If these items belong to a specific class (:UserSuitability), then I need to check if the user is also from the same class. There are four possible scenarios:

  1. The item is from a class that is rdfs:subClassOf :UserSuitability, and the user is also from the same class. Then check if the item contains a value for hasSuitabilityValue and assign it to the variable ?suitabilityValue.
  2. The item is from a class that is rdfs:subClassOf :UserSuitability, but the user is not, then check if the item contains a value for hasSuitabilityNotValue and assign it to the variable ?suitabilityValue.
  3. The item is not from a class that is rdfs:subClassOf: UserSuitability, then assign 1 to the variable ?suitabilityValue.
    1. The item is not from a class that is rdfs:subClassOf:UserSuitability, and neither is the user. In this case, do nothing.

My query so far, and the data to test it with are provided below. Note that :item1 in the data should match the left hand side of the union, while :item2 should match the right hand side. It seems that the right hand side never matches.

Data

@prefix : <http://www.semanticrecommender.com/rs#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

:user1 a :class1 .
:user1 :likes :item1 .
:user1 :likes :item2 .
:class1 rdfs:subClassOf :UserSuitability .
:item1 a :class1 .
:item1 :hasSuitabilityWeight 1.5 .
:item1 :hasNotSuitabilityWeight 0.5 .
:item2 a :class2 .
:class2 rdfs:subClassOf :UserSuitability .

Query

prefix : <http://www.semanticrecommender.com/rs#>
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>

select ?item ?suitabilityValue where
{
  values ?user {:user1}
  ?user :likes ?item

  optional{
    #-- check if the item is from a class that is
    #-- rdfs:subClassOf :UserSuitability
    ?item a ?suitabilityClass.
    ?suitabilityClass rdfs:subClassOf :UserSuitability.
    {
      #-- if the user is also from the same class
      ?user a ?suitabilityClass
        optional{
        #-- check if the item has a value for :hasSuitabilityWeight
        ?item :hasSuitabilityWeight ?suitabilityValueOptional.
      }
      #-- if it does, assign it to the variable
      #-- ?suitabilityValue, otherwise, assign 1
      #-- to the variable suitabilityValue
      bind(if(bound(?suitabilityValueOptional), ?suitabilityValueOptional, 1) as ?suitabilityValue)

    }
    union
    {
      #-- if the user is not from the same class
      filter not exists {?user a ?suitabilityClass}
      optional{
        #-- if the item has a value to hasNotSuitabilityWeight
        ?item :hasNotSuitabilityWeight ?suitabilityNotValueOptional.
      }
      #-- assign it to suitabilityValue, otherwise, assign 0
      bind(if(bound(?suitabilityNotValueOptional), ?suitabilityNotValueOptional, 0) as ?suitabilityValue)
    }
  }
}

Solution

  • So, I tried to recreate this problem from scratch, and ended up with data that's very similar to yours, although a little bit simpler. Here's the data I ended up using, which lets me avoid some of the filtering on whether a class is actually suitable or not.

    @prefix : <urn:ex:>
    
    :user a :A ;
          :likes :i , :j .
    
    :i a :A ;
       :hasValueYes 1 ;
       :hasValueNo  2 .
    
    :j a :B .
    

    Now, here's the query that's like yours; it only gets a result for one of the two items, even though it seems like the other should match. There's one commented line that I'll explain afterward.

    prefix : <urn:ex:>
    
    select ?item ?value {
      values ?user { :user }
    
      ?user :likes ?item .
      ?item a ?itemClass .
    
      {
        ?user a ?itemClass
        optional {
          ?item :hasValueYes ?valueYes
        }
        bind(if(bound(?valueYes), ?valueYes, "default yes value") as ?value)
      }
      union
      {
        #-- ?item ?anyP ?anyO   # (***)
        filter not exists { ?user a ?itemClass }
        optional {
          ?item :hasValueNo ?valueNo
        }
        bind(if(bound(?valueNo), ?valueNo, "default no value") as ?value)
      }
    }  
    
    ----------------
    | item | value |
    ================
    | :i   | 1     |
    ----------------
    

    Now, there's one commented line in that query:

    #-- ?item ?anyP ?anyO   # (***)
    

    If you uncomment that line, you get the results that you'd expect:

    -----------------------------
    | item | value              |
    =============================
    | :i   | 1                  |
    | :j   | "default no value" |
    -----------------------------
    

    I think that what's happening here is that in the second optional case, since there are no triple patterns that introduce bindings (since the optional doesn't match), that side of the union doesn't get included, even though the bind would introduce some bindings if the side were allowed to match. By adding the pattern:

    ?item ?anyP ?anyO
    

    to the query, there's at least something that will match in that part of the union, at which point the rest of the block gets included. I used ?anyP and ?anyO to emphasize that it's arbitrary, but since you already know that the ?item is an ?itemClass, you could just include that triple again, i.e., ?item a ?itemClass.

    So, in your case, if you just add

    ?user :likes ?item
    

    to the right hand union block, you'll get the results that you're expecting:

    -----------------------------
    | item   | suitabilityValue |
    =============================
    | :item2 | 0                |
    | :item1 | 1.5              |
    -----------------------------