Last active
September 20, 2016 13:58
-
-
Save wsalesky/d7236a8213008a4cf8dd8ca578eabd6b to your computer and use it in GitHub Desktop.
Partial facet implementation for eXist-db based on the EXPath specifications (http://expath.org/spec/facet)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
xquery version "3.0"; | |
(:~ | |
: Partial facet implementation for eXist-db based on the EXPath specifications (http://expath.org/spec/facet) | |
: | |
: Uses the following eXist-db specific functions: | |
: util:eval | |
: request:get-parameter | |
: request:get-parameter-names() | |
: | |
: @author Winona Salesky | |
: @version 1.0 | |
: | |
: @see http://expath.org/spec/facet | |
: | |
: TODO: | |
: Handle arrays in attribute values, see tei:relation/@mutual for an example | |
: Support for hierarchical facets | |
:) | |
module namespace facet = "http://expath.org/ns/facet"; | |
import module namespace functx="http://www.functx.com"; | |
declare namespace tei = "http://www.tei-c.org/ns/1.0"; | |
(: External facet parameters :) | |
declare variable $facet:fq {request:get-parameter('fq', '') cast as xs:string}; | |
(:~ | |
: Given a result sequence, and a sequence of facet definitions, count the facet-values for each facet defined by the facet definition(s). | |
: Accepts one or more facet:facet-definition elements | |
: Signiture: | |
facet:count($results as item()*, | |
$facet-definitions as element(facet:facet-definition)*) as element(facet:facets) | |
: @param $results results node to be faceted on. | |
: @param $facet-definitions one or more facet:facet-definition element | |
:) | |
declare function facet:count($results as item()*, $facet-definitions as element(facet:facet-definition)*) as element(facet:facets){ | |
<facets xmlns="http://expath.org/ns/facet"> | |
{ | |
for $facet in $facet-definitions | |
return | |
<facet name="{$facet/@name}"> | |
{ | |
(:let $facets := facet:facet($results, $facet):) | |
(:let $total := count(facet:facet($results, $facet)):) | |
let $max := if($facet/descendant::facet:max-values/text()) then $facet/descendant::facet:max-values/text() else 100 | |
for $facets at $i in subsequence(facet:facet($results, $facet),1,$max) | |
return $facets | |
} | |
</facet> | |
} | |
</facets> | |
}; | |
(:~ | |
: Given a result sequence, and a facet definition, count the facet-values for each facet defined by the facet definition. | |
: Facet defined by facets:facet-definition/facet:group-by/facet:sub-path | |
: @param $results results to be faceted on. | |
: @param $facet-definitions one or more facet:facet-definition element | |
:) | |
(: TODO: Handle nested facet-definition :) | |
declare function facet:facet($results as item()*, $facet-definitions as element(facet:facet-definition)?) as item()*{ | |
if($facet-definitions/facet:range) then | |
facet:group-by-range($results, $facet-definitions) | |
else if ($facet-definitions/facet:group-by/@function) then | |
util:eval(concat($facet-definitions/facet:group-by/@function,'($results,$facet-definitions)')) | |
else facet:group-by($results, $facet-definitions) | |
}; | |
(:~ | |
: Given a result sequence, and a facet definition, count the facet-values for each facet defined by the facet definition. | |
: Facet defined by facets:facet-definition/facet:group-by/facet:sub-path | |
: @param $results results to be faceted on. | |
: @param $facet-definitions one or more facet:facet-definition element | |
:) | |
(: TODO: Need to be able to switch out descending with ascending based on facet-def/order-by/@direction:) | |
declare function facet:group-by($results as item()*, $facet-definitions as element(facet:facet-definition)?) as element(facet:key)*{ | |
let $path := concat('$results/',$facet-definitions/facet:group-by/facet:sub-path/text()) | |
let $sort := $facet-definitions/facet:order-by | |
for $f in util:eval($path) | |
group by $facet-grp := $f | |
order by | |
if($sort/text() = 'value') then $f[1] | |
else count($f) | |
descending | |
return <key xmlns="http://expath.org/ns/facet" count="{count($f)}" value="{$f[1]}" label="{$f[1]}"/> | |
}; | |
(:~ | |
: Syriaca.org specific group-by function for correctly labeling submodules. | |
:) | |
declare function facet:group-by-sub-module($results as item()*, $facet-definitions as element(facet:facet-definition)?) { | |
let $path := concat('$results/',$facet-definitions/facet:group-by/facet:sub-path/text()) | |
let $sort := $facet-definitions/facet:order-by | |
for $f in util:eval($path) | |
let $label := | |
if($f[1] = 'http://syriaca.org/authors') then 'Authors' | |
else if($f[1] = 'http://syriaca.org/q') then 'Saints' | |
else () | |
group by $facet-grp := $f | |
order by | |
if($sort/text() = 'value') then $f[1] | |
else count($f) | |
descending | |
return <key xmlns="http://expath.org/ns/facet" count="{count($f)}" value="{$f[1]}" label="{$label[1]}"/> | |
}; | |
(:~ | |
: Syriaca.org specific group-by function for correctly labeling attributes with arrays. | |
:) | |
declare function facet:group-by-array($results as item()*, $facet-definitions as element(facet:facet-definition)?){ | |
let $path := concat('$results/',$facet-definitions/facet:group-by/facet:sub-path/text()) | |
let $sort := $facet-definitions/facet:order-by | |
let $d := tokenize(string-join(util:eval($path),' '),' ') | |
for $f in $d | |
group by $facet-grp := tokenize($f,' ') | |
order by | |
if($sort/text() = 'value') then $f[1] | |
else count($f) | |
descending | |
return <key xmlns="http://expath.org/ns/facet" count="{count($f)}" value="{$f[1]}" label="{$f[1]}"/> | |
}; | |
(:~ | |
: Given a result sequence, and a facet definition, count the facet-values for each range facet defined by the facet definition. | |
: Range values defined by: range and range/bucket elements | |
: Facet defined by facets:facet-definition/facet:group-by/facet:sub-path | |
: @param $results results to be faceted on. | |
: @param $facet-definitions one or more facet:facet-definition element | |
:) | |
declare function facet:group-by-range($results as item()*, $facet-definitions as element(facet:facet-definition)*) as element(facet:key)*{ | |
let $ranges := $facet-definitions/facet:range | |
let $sort := $facet-definitions/facet:order-by | |
for $range in $ranges/facet:bucket | |
let $path := concat('$results/',$facet-definitions/descendant::facet:sub-path/text(),'[. gt "', facet:type($range/@gt, $ranges/@type),'" and . lt "',facet:type($range/@lt, $ranges/@type),'"]') | |
let $f := util:eval($path) | |
order by | |
if($sort/text() = 'value') then $f[1] | |
else count($f) | |
descending | |
return | |
<key xmlns="http://expath.org/ns/facet" count="{count($f)}" value="{string($range/@name)}" label="{string($range/@name)}"/> | |
}; | |
(:~ | |
: Adds type casting when type is specified facet:facet:group-by/@type | |
: @param $value of xpath | |
: @param $type value of type attribute | |
:) | |
declare function facet:type($value as item()*, $type as xs:string?) as item()*{ | |
if($type != '') then | |
if($type = 'xs:string') then xs:string($value) | |
else if($type = 'xs:string') then xs:string($value) | |
else if($type = 'xs:decimal') then xs:decimal($value) | |
else if($type = 'xs:integer') then xs:integer($value) | |
else if($type = 'xs:long') then xs:long($value) | |
else if($type = 'xs:int') then xs:int($value) | |
else if($type = 'xs:short') then xs:short($value) | |
else if($type = 'xs:byte') then xs:byte($value) | |
else if($type = 'xs:float') then xs:float($value) | |
else if($type = 'xs:double') then xs:double($value) | |
else if($type = 'xs:dateTime') then xs:dateTime($value) | |
else if($type = 'xs:date') then xs:date($value) | |
else if($type = 'xs:gYearMonth') then xs:gYearMonth($value) | |
else if($type = 'xs:gYear') then xs:gYear($value) | |
else if($type = 'xs:gMonthDay') then xs:gMonthDay($value) | |
else if($type = 'xs:gMonth') then xs:gMonth($value) | |
else if($type = 'xs:gDay') then xs:gDay($value) | |
else if($type = 'xs:duration') then xs:duration($value) | |
else if($type = 'xs:anyURI') then xs:anyURI($value) | |
else if($type = 'xs:Name') then xs:Name($value) | |
else $value | |
else $value | |
}; | |
(:~ | |
: XPath filter to be passed to main query | |
: creates XPath based on facet:facet-definition//facet:sub-path. | |
: @param $facet-def facet:facet-definition element | |
: NOTE: need to do type checking here | |
: NOTE: add range handling here. | |
:) | |
declare function facet:facet-filter($facet-definitions as node()*) as item()*{ | |
if($facet:fq != '') then | |
string-join( | |
for $facet in tokenize($facet:fq,';fq-') | |
let $facet-name := substring-before($facet,':') | |
let $facet-value := normalize-space(substring-after($facet,':')) | |
return | |
for $facet in $facet-definitions/facet:facet-definition[@name = $facet-name] | |
let $path := | |
if(matches($facet/descendant::facet:sub-path/text(), '^/@')) then concat('descendant::*/',substring($facet/descendant::facet:sub-path/text(),2)) | |
else $facet/descendant::facet:sub-path/text() | |
return | |
if($facet-value != '') then | |
if($facet/facet:range) then | |
concat('[',$path,'[string(.) gt "', facet:type($facet/facet:range/facet:bucket[@name = $facet-value]/@gt, $facet/facet:range/facet:bucket[@name = $facet-value]/@type),'" and string(.) lt "',facet:type($facet/facet:range/facet:bucket[@name = $facet-value]/@lt, $facet/facet:range/facet:bucket[@name = $facet-value]/@type),'"]]') | |
else if($facet/facet:group-by[@function="facet:group-by-array"]) then | |
concat('[',$path,'[matches(., "',$facet-value,'(\W|$)")]',']') | |
else concat('[',$path,'[string(.) = "',$facet-value,'"]',']') | |
else(),'') | |
else () | |
}; | |
(:~ | |
: Builds new facet params for html links. | |
: Uses request:get-parameter-names() to get all current params | |
:) | |
declare function facet:url-params(){ | |
string-join(for $param in request:get-parameter-names() | |
return | |
if($param = 'fq') then () | |
else if(request:get-parameter($param, '') = ' ') then () | |
else concat('&',$param, '=',request:get-parameter($param, '')),'') | |
}; | |
(: HTML display functions :) | |
(:~ | |
: Create 'Remove' button | |
: Constructs new URL for user action 'remove facet' | |
:) | |
declare function facet:selected-facets-display(){ | |
for $facet in tokenize($facet:fq,';fq-') | |
let $value := substring-after($facet,':') | |
let $new-fq := string-join( | |
for $facet-param in tokenize($facet:fq,';fq-') | |
return | |
if($facet-param = $facet) then () | |
else concat(';fq-',$facet-param),'') | |
let $href := if($new-fq != '') then concat('?fq=',replace(replace($new-fq,';fq- ',''),';fq-;fq-',';fq-'),facet:url-params()) else () | |
return | |
if($facet != '') then | |
<span class="label label-facet" title="Remove {$value}"> | |
{$value} <a href="{$href}" class="facet icon"> x</a> | |
</span> | |
else() | |
}; | |
(:~ | |
: Create 'Add' button | |
: Constructs new URL for user action 'Add facet' | |
:) | |
declare function facet:html-list-facets-as-buttons($facets as node()*){ | |
for $f in $facets/facet:facet | |
return | |
<div class="facet-grp"> | |
<h4>{string($f/@name)}</h4> | |
{ | |
for $key in $f/facet:key | |
let $facet-query := replace(replace(concat(';fq-',string($f/@name),':',string($key/@value)),';fq-;fq-;',';fq-'),';fq- ','') | |
let $new-fq := | |
if($facet:fq) then concat('fq=',$facet:fq,$facet-query) | |
else concat('fq=',$facet-query) | |
return <a href="?{$new-fq}{facet:url-params()}" class="facet-label btn btn-default">{string($key/@label)} <span class="count"> ({string($key/@count)})</span></a> | |
} | |
</div> | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
xquery version "3.0"; | |
(:~ | |
: Partial facet implementation for eXist-db based on the EXPath specifications (http://expath.org/spec/facet) | |
: | |
: Uses the following eXist-db specific functions: | |
: util:eval | |
: | |
: @author Winona Salesky | |
: @version 1.0 | |
: | |
: @see http://expath.org/spec/facet | |
: | |
:) | |
import module namespace facets = "http://expath.org/ns/facet" at "facet.xqm"; | |
declare namespace tei = "http://www.tei-c.org/ns/1.0"; | |
declare namespace facet = "http://expath.org/ns/facet"; | |
(: | |
declare namespace output = "http://www.w3.org/2010/xslt-xquery-serialization"; | |
declare option output:method "html5"; | |
declare option output:media-type "text/html"; | |
:) | |
(: | |
: Example facet definition. Uses Srophe data: https://github.com/srophe/srophe-app-data | |
:) | |
let $facet-def := | |
<facets xmlns="http://expath.org/ns/facet"> | |
<facet-definition name="Place Name"> | |
<group-by> | |
<sub-path>descendant::tei:placeName[@xml:lang = 'en']/descendant-or-self::text()</sub-path> | |
</group-by> | |
<max-values>2</max-values> | |
<order-by direction="descending">count</order-by> | |
</facet-definition> | |
<facet-definition name="Title"> | |
<group-by> | |
<sub-path>descendant::tei:bibl/tei:title/descendant-or-self::text()</sub-path> | |
</group-by> | |
<max-values>1</max-values> | |
<order-by direction="ascending">count</order-by> | |
</facet-definition> | |
<facet-definition name="PersRefs"> | |
<group-by> | |
<sub-path>descendant::tei:persName/@ref</sub-path> | |
</group-by> | |
<max-values>1</max-values> | |
<order-by direction="ascending">count</order-by> | |
</facet-definition> | |
<facet-definition name="Keywords"> | |
<group-by> | |
<sub-path>descendant::*/@target[contains(.,'/keyword/')]</sub-path> | |
</group-by> | |
<max-values>5</max-values> | |
<order-by direction="ascending">count</order-by> | |
</facet-definition> | |
<facet-definition name="Dates"> | |
<range type="xs:date"> | |
<bucket lt="0400-01-01" gt="0300-01-01" name="300-400"/> | |
<bucket lt="0300-01-01" gt="0200-01-01" name="200-300"/> | |
</range> | |
<group-by type="xs:date"> | |
<sub-path>descendant::tei:death/@syriaca-computed-start</sub-path> | |
</group-by> | |
<max-values>5</max-values> | |
<order-by direction="ascending">count</order-by> | |
</facet-definition> | |
</facets> | |
let $collection := util:eval(concat("collection('/db/apps/srophe-data/data')//tei:body",facet:facet-filter($facet-def))) | |
return | |
<div total="{count($collection)}" filterpath="{facet:facet-filter($facet-def)}"> | |
{( | |
facet:selected-facets-display(), | |
(:facet:html-list-facets-as-buttons(facet:facets($facet-def, $collection)),:) | |
facet:count($collection, $facet-def/child::*), | |
<div class="results">{$collection[1]}</div> | |
)} | |
</div> | |
Added support for arrays within attributes and also for using local group-by functions. Sample group-by functions 'facet:group-by-sub-module' which allows for easier labeling for Syriaca.org submodules/person types.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Just started re-reading the specs, current code is not very compliant with the specs. So more just facet POC.