Tutorial - Manipulando SPARQL usando ARQ

Quando você começa a trabalhar com SPARQL você rapidamente descobre que queries estáticas são restritivas. Talvez você queira mudar um valor, adicionar um filtro, alterar o limite, etc. Sendo do tipo impaciente, você começa a manipular a string da query e isso funciona. Mas o que dizer de little Bobby Tables? Além do mais, mesmo que você limpe ao máximo suas entradas, manipulação de strings é um processo tenso e erros de sintaxe esperam por você. Muito embora possa parecer mais difícil do que string munging, a API ARQ é sua amiga na longa jornada.

Originalmente publicado em Research Revealed project blog

Inserindo valores (comandos simples prontos)

Vamos começar com algo simples. Suponha que nós queiramos restringir a query a seguir a uma pessoa (person) em particular:

   select * { ?person <http://xmlns.com/foaf/0.1/name> ?name }

String#replaceAll deveria funcionar, mas existe um jeito mais seguro. QueryExecutionFactory em muitos casos, permite que você alimente uma QuerySolution com a qual você pode prefixar valores.

   QuerySolutionMap initialBinding = new QuerySolutionMap();
   initialBinding.add("name", personResource);
   qe = QueryExecutionFactory.create(query, dataset, initialBinding);

Isto geralmente é muito mais simples do que a string equivalente desde que você não tenha usar aspas para citações. (Esteja ciente de que isto não funciona para sparqlService, o que é uma pena. Seria legal gastar algum tempo consertando isto.)

Fazendo uma Query a partir do zero

As limitações previamente mencionadas se devem ao fato de que prefixação na verdade não muda a query em nada, apenas a execução daquela query. Então, como nós realmente alteramos queries?

ARQ provê duas maneiras de se trabalhar com queries: no nível de sintaxe (Query and Element), ou no nível algébrico (Op). A distinção entre eles fica claro com os filtros:

   SELECT ?s { ?s <http://example.com/val> ?val . FILTER ( ?val < 20 ) }

Se você trabalha no nível de sintaxe, você irá descobrir que isso (em pseudo código) se parece com :

   (GROUP (PATTERN ( ?s <http://example.com/val> ?val )) (FILTER ( < ?val 20 ) ))

Isto é, existe um grupo contendo um padrão triplo e um filtro, do mesmo jeito que você vê na query. A álgebra é diferente e nós podemos vê-la usando arq.qparse --print op

   $ java arq.qparse --print op 'SELECT ?s { ?s <http://example.com/val> ?val . FILTER ( ?val < 20 ) }'
   (base <file:///...>
       (project (?s)
           (filter (< ?val 20)
               (bgp (triple ?s <http://example.com/val> ?val)))))

Aqui o filtro contém o padrão, ao invés de se situar próximo a ele. Esta forma torna claro que a expressão está filtrando o padrão.

Vamos criar esta query do zero usando ARQ. Nós começamos com algumas partes comuns: a tripla a ser comparada e a expressão a ser filtrada.

   // ?s ?p ?o .
   Triple pattern =
       Triple.create(Var.alloc("s"), Var.alloc("p"), Var.alloc("o"));
   // ( ?s < 20 )
   Expr e = new E_LessThan(new ExprVar("s"), new NodeValueInteger(20));

Triple deveria ser familiar de Jena. Var é uma extensão de Node para variáveis. Expr é a interface raíz para expressões, aquelas coisas que aparecem em FILTER and LET.

Primeiro, o caminho da sintaxe:

   ElementTriplesBlock block = new ElementTriplesBlock(); // Make a BGP
   block.addTriple(pattern);                              // Add our pattern match
   ElementFilter filter = new ElementFilter(e);           // Make a filter matching the expression
   ElementGroup body = new ElementGroup();                // Group our pattern match and filter
   body.addElement(block);
   body.addElement(filter);

   Query q = QueryFactory.make();
   q.setQueryPattern(body);                               // Set the body of the query to our group
   q.setQuerySelectType();                                // Make it a select query
   q.addResultVar("s");                                   // Select ?s

Agora a álgebra:

   Op op;
   BasicPattern pat = new BasicPattern();                 // Make a pattern
   pat.add(pattern);                                      // Add our pattern match
   op = new OpBGP(pat);                                   // Make a BGP from this pattern
   op = OpFilter.filter(e, op);                           // Filter that pattern with our expression
   op = new OpProject(op, Arrays.asList(Var.alloc("s"))); // Reduce to just ?s
   Query q = OpAsQuery.asQuery(op);                       // Convert to a query
   q.setQuerySelectType();                                // Make is a select query

Note que o tipo da query (SELECT, CONSTRUCT, DESCRIBE, ASK)não é parte da álgebra, e que nós temos que configurar isso na query (embora SELECT seja o padrão). FROM e FROM NAMED estão igualmente ausentes.

Você também pode olhar para a álgebra e a sintaxe usando visitors. Comece estendendo OpVisitorBase (ElementVisitorBase) que apaga a interface de modo que você pode se concentrar nas partes de interesse, então dê um passo a frente e use OpWalker.walk(Op, OpVisitor) (ElementWalker.walk(Element, ElementVisitor)). Isso funciona no esquema “bottom up” (de baixo para cima).

Para algumas alterações, como manipulação de padrões triplos no local, visitors irão trabalhar bem. Eles provêm um jeito simples de manipular as partes certas da query e você pode alterar as BGPs backing padrões tanto na álgebra quanto na sintaxe. Entretanto, mutações (mutation) não estão consistentemente disponíveis, não conte com elas.

Transformando a Álgebra

A primeira vista, não há vantagens óbvias em usar a álgebra. O real poder fica claro com o uso de transformers (transformações), que lhe permitem reorganizar uma álgebra completamente. ARQ faz amplo uso de transformers para simplificar e aperfeiçoar execuções de query.

Em Research Revealed (Pesquisa revelada, em tradução livre), eu escrevi algum código para pegar certo número de constraints (constantes) e produzir uma query. Havia várias formas de se fazer isto, mas o jeito que eu achei foi gerar ops de cada constraint e juntar o resultado.

   for (Constraint con: cons) {
       op = OpJoin.create(op, consToOp(cons)); // join
   }

O resultado foi uma bagunça incrivelmente correta, que é remotamente compreensível em apenas três condições:

   (join
       (join
           (filter (< ?o0 20) (bgp (triple ?s <urn:ex:prop0> ?o0)))
           (filter (< ?o1 20) (bgp (triple ?s <urn:ex:prop1> ?o1))))
       (filter (< ?o2 20) (bgp (triple ?s <urn:ex:prop2> ?o2))))

Cada uma das constraints é um filtro e um bgp. Isso pode ser muito mais compreensível removendo os filtros e juntando (merging) os padrões triplos. Nós podemos fazer isso usando Transform:

   class QueryCleaner extends TransformBase
   {
       @Override
       public Op transform(OpJoin join, Op left, Op right) {
           // Bail if not of the right form
           if (!(left instanceof OpFilter && right instanceof OpFilter)) return join;
           OpFilter leftF = (OpFilter) left;
           OpFilter rightF = (OpFilter) right;

           // Add all of the triple matches to the LHS BGP
           ((OpBGP) leftF.getSubOp()).getPattern().addAll(((OpBGP) rightF.getSubOp()).getPattern());
           // Add the RHS filter to the LHS
           leftF.getExprs().addAll(rightF.getExprs());
           return leftF;
       }
   }
   ...
   op = Transformer.transform(new QueryCleaner(), op); // clean query

O código abaixo procura pelos joins do formulário:

   (join
       (filter (exp1) (bgp1))
       (filter (exp2) (bgp2)))

E substitui ele com:

   (filter (exp1 && exp2) (bgp1 && bgp2))

Enquanto nós percorremos a query original, todos os joins são removidos e o resultado final é:

   (filter (exprlist (< ?o0 20) (< ?o1 20) (< ?o2 20))
       (bgp
           (triple ?s <urn:ex:prop0> ?o0)
           (triple ?s <urn:ex:prop1> ?o1)
           (triple ?s <urn:ex:prop2> ?o2)
   ))

Isto completa esta breve introdução. Existe muito mais em ARQ, claro, mas esperamos que você tenha tido um gostinho do que ele pode fazer.