From 0d6f8f643793a196ce2d9d01af6df97f8be52961 Mon Sep 17 00:00:00 2001 From: Martin Middel Date: Wed, 28 Aug 2024 15:44:27 +0200 Subject: [PATCH] Clean up configuration for facets Config in its separate file. And move over related files from tei-publisher --- profiles/base10/modules/config.xqm | 28 ---- profiles/base10/modules/facets-config.xql | 33 ++++ profiles/base10/modules/facets.xql | 166 +++++++++++++++++++++ profiles/base10/modules/lib/api/search.xql | 150 +++++++++++++++++++ 4 files changed, 349 insertions(+), 28 deletions(-) create mode 100644 profiles/base10/modules/facets-config.xql create mode 100644 profiles/base10/modules/facets.xql create mode 100644 profiles/base10/modules/lib/api/search.xql diff --git a/profiles/base10/modules/config.xqm b/profiles/base10/modules/config.xqm index 5800fcd..b59bd3d 100644 --- a/profiles/base10/modules/config.xqm +++ b/profiles/base10/modules/config.xqm @@ -117,34 +117,6 @@ declare variable $config:pagination-depth := $gen:pagination-depth; :) declare variable $config:pagination-fill := $gen:pagination-fill; -(: - : Display configuration for facets to be shown in the sidebar. The facets themselves - : are configured in the index configuration, collection.xconf. - :) -declare variable $config:facets := [ - map { - "dimension": "genre", - "heading": "facets.genre", - "max": 5, - "hierarchical": true() - }, - map { - "dimension": "language", - "heading": "facets.language", - "max": 5, - "hierarchical": false(), - "output": function($label) { - switch($label) - case "de" return "German" - case "es" return "Spanish" - case "la" return "Latin" - case "fr" return "French" - case "en" return "English" - default return $label - } - } -]; - (: : The function to be called to determine the next content chunk to display. : It takes two parameters: diff --git a/profiles/base10/modules/facets-config.xql b/profiles/base10/modules/facets-config.xql new file mode 100644 index 0000000..8fee875 --- /dev/null +++ b/profiles/base10/modules/facets-config.xql @@ -0,0 +1,33 @@ +module namespace facets-config="http://teipublisher.com/api/facets-config"; + +import module namespace config="http://www.tei-c.org/tei-simple/config" at "config.xqm"; + +declare namespace tei="http://www.tei-c.org/ns/1.0"; + +declare function facets-config:get-name($id as xs:string, $type as xs:string) as xs:string { + let $entity := collection($config:register-root)/id($id) + return head(( + switch ($type) + case 'place' return head(($entity//tei:placeName[@type = "main"], $entity//tei:placeName)) + case 'actor' return head(($entity//(tei:persName | tei:orgName)[@type = "main"], $entity//tei:placeName)) + default return "ERR", + "Unresolvable entity " || $id || " of type " || $type) + ) +}; + +declare %public variable $facets-config:facets as array(*) := [ + map { + "dimension": "place", + "heading": "facets.place", + "max": 5, + "hierarchical": false(), + "output": facets-config:get-name(?, 'place') + }, + map { + "dimension": "actor", + "heading": "facets.actor", + "max": 5, + "hierarchical": false(), + "output": facets-config:get-name(?, 'actor') + } +]; diff --git a/profiles/base10/modules/facets.xql b/profiles/base10/modules/facets.xql new file mode 100644 index 0000000..f734a12 --- /dev/null +++ b/profiles/base10/modules/facets.xql @@ -0,0 +1,166 @@ +(: + : + : Copyright (C) 2019 Wolfgang Meier + : + : This program is free software: you can redistribute it and/or modify + : it under the terms of the GNU General Public License as published by + : the Free Software Foundation, either version 3 of the License, or + : (at your option) any later version. + : + : This program is distributed in the hope that it will be useful, + : but WITHOUT ANY WARRANTY; without even the implied warranty of + : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + : GNU General Public License for more details. + : + : You should have received a copy of the GNU General Public License + : along with this program. If not, see . + :) +xquery version "3.1"; + +module namespace facets="http://teipublisher.com/facets"; + +import module namespace config="http://www.tei-c.org/tei-simple/config" at "config.xqm"; + +declare namespace tei="http://www.tei-c.org/ns/1.0"; + +declare function facets:sort($config as map(*), $lang as xs:string?, $facets as map(*)?) { + array { + if (exists($facets)) then + for $key in map:keys($facets) + let $value := map:get($facets, $key) + let $sortKey := facets:translate($config, $lang, $key) + order by $sortKey ascending + return + map { $key: $value } + else + () + } +}; + +declare function facets:print-table($config as map(*), $nodes as element()+, $values as xs:string*, $params as xs:string*) { + let $all := exists($config?max) and facets:get-parameter("all-" || $config?dimension) + let $lang := tokenize(facets:get-parameter("language"), '-')[1] + let $count := if ($all) then 50 else $config?max + let $facets := + if (exists($values)) then + ft:facets($nodes, $config?dimension, $count, $values) + else + ft:facets($nodes, $config?dimension, $count) + return + if (map:size($facets) > 0) then + + { + array:for-each(facets:sort($config, $lang, $facets), function($entry) { + map:for-each($entry, function($label, $freq) { + let $content := facets:translate($config, $lang, $label) + return + + + + , + if (empty($params)) then + () + else + let $nested := facets:print-table($config, $nodes, ($values, head($params)), tail($params)) + return + if ($nested and head($params) eq $label) then + + + + else + () + }) + }) + } +
+ + { if ($label = $params) then attribute checked { "checked" } else () } + {$content} + + {$freq}
+ {$nested} +
+ else + () +}; + +declare function facets:display($config as map(*), $nodes as element()+) { + let $params := facets:get-parameter("facet-" || $config?dimension) + let $lang := tokenize(facets:get-parameter("language"), '-')[1] + let $table := facets:print-table($config, $nodes, (), $params) + + let $maxcount := 50 + (: maximum number shown :) + let $max := head(($config?max, 50)) + + (: facet count for current values selected :) + let $fcount := + map:size( + if (count($params)) then + ft:facets($nodes, $config?dimension, $maxcount, $params) + else + ft:facets($nodes, $config?dimension, $maxcount) + ) + + where $table + return ( +
+

{$config?heading} + { + if ($fcount > $max) then + + { if (facets:get-parameter("all-" || $config?dimension)) then attribute checked { "checked" } else () } + Show top 50 + + else + () + } +

+ { + $table, + (: if config specifies a property "source", output combo-box :) + if (map:contains($config, "source")) then + (: use source as URL to API endpoint from which to retrieve possible values :) + + + + else + () + } +
+ ) +}; + +declare function facets:get-parameter($name as xs:string) { + let $param := request:get-parameter($name, ()) + return + if (exists($param)) then + $param + else + let $fromSession := session:get-attribute($config:session-prefix || '.params') + return + if (exists($fromSession)) then + $fromSession?($name) + else + () +}; + +declare function facets:translate($config as map(*)?, $language as xs:string?, $label as xs:string) { + if (exists($config) and map:contains($config, "output")) then + let $fn := $config?output + return + if (function-arity($fn) = 2) then + $fn($label, $language) + else + $fn($label) + else + $label +}; \ No newline at end of file diff --git a/profiles/base10/modules/lib/api/search.xql b/profiles/base10/modules/lib/api/search.xql new file mode 100644 index 0000000..3db0c49 --- /dev/null +++ b/profiles/base10/modules/lib/api/search.xql @@ -0,0 +1,150 @@ +xquery version "3.1"; + +module namespace sapi="http://teipublisher.com/api/search"; + +import module namespace query="http://www.tei-c.org/tei-simple/query" at "../../query.xql"; +import module namespace nav="http://www.tei-c.org/tei-simple/navigation" at "../../navigation.xql"; +import module namespace config="http://www.tei-c.org/tei-simple/config" at "../../config.xqm"; +import module namespace tpu="http://www.tei-c.org/tei-publisher/util" at "../util.xql"; +import module namespace kwic="http://exist-db.org/xquery/kwic" at "resource:org/exist/xquery/lib/kwic.xql"; +import module namespace facets="http://teipublisher.com/facets" at "../../facets.xql"; +import module namespace facets-config="http://teipublisher.com/api/facets-config" at "../../facets-config.xqm"; + +declare namespace tei="http://www.tei-c.org/ns/1.0"; + +declare function sapi:autocomplete($request as map(*)) { + let $q := request:get-parameter("query", ()) + let $type := request:get-parameter("field", "text") + let $doc := request:get-parameter("doc", ()) + let $items := + if ($q) then + query:autocomplete($doc, $type, $q) + else + () + return + array { + for $item in $items + group by $item + return + map { + "text": $item, + "value": $item + } + } +}; + +declare function sapi:search($request as map(*)) { + (:If there is no query string, fill up the map with existing values:) + if (empty($request?parameters?query)) + then + sapi:show-hits($request, session:get-attribute($config:session-prefix || ".hits"), session:get-attribute($config:session-prefix || ".docs")) + else + (:Otherwise, perform the query.:) + (: Here the actual query commences. This is split into two parts, the first for a Lucene query and the second for an ngram query. :) + (:The query passed to a Luecene query in ft:query is an XML element containing one or two . The contain the original query and the transliterated query, as indicated by the user in $query-scripts.:) + let $hitsAll := + (:If the $query-scope is narrow, query the elements immediately below the lowest div in tei:text and the four major element below tei:teiHeader.:) + for $hit in query:query-default($request?parameters?field, $request?parameters?query, $request?parameters?doc, ()) + order by ft:score($hit) descending + return $hit + let $hitCount := count($hitsAll) + let $hits := if ($hitCount > 1000) then subsequence($hitsAll, 1, 1000) else $hitsAll + (:Store the result in the session.:) + let $store := ( + session:set-attribute($config:session-prefix || ".hits", $hitsAll), + session:set-attribute($config:session-prefix || ".hitCount", $hitCount), + session:set-attribute($config:session-prefix || ".search", $request?parameters?query), + session:set-attribute($config:session-prefix || ".field", $request?parameters?field), + session:set-attribute($config:session-prefix || ".docs", $request?parameters?doc) + ) + return + sapi:show-hits($request, $hits, $request?parameters?doc) +}; + +declare %private function sapi:show-hits($request as map(*), $hits as item()*, $docs as xs:string*) { + response:set-header("pb-total", xs:string(count($hits))), + response:set-header("pb-start", xs:string($request?parameters?start)), + for $hit at $p in subsequence($hits, $request?parameters?start, $request?parameters?per-page) + let $config := tpu:parse-pi(root($hit), $request?parameters?view) + let $parent := query:get-parent-section($config, $hit) + let $parent-id := config:get-identifier($parent) + let $parent-id := if (exists($docs)) then replace($parent-id, "^.*?([^/]*)$", "$1") else $parent-id + let $div := query:get-current($config, $parent) + let $expanded := util:expand($hit, "add-exist-id=all") + let $docId := config:get-identifier($div) + return + +
+
{$request?parameters?start + $p - 1}
+ { query:get-breadcrumbs($config, $hit, $parent-id) } +
+
+ { + for $match in subsequence($expanded//exist:match, 1, 5) + let $matchId := $match/../@exist:id + let $docLink := + if ($config?view = "page") then + (: first check if there's a pb in the expanded section before the match :) + let $pbBefore := $match/preceding::tei:pb[1] + return + if ($pbBefore) then + $pbBefore/@exist:id + else + (: no: locate the element containing the match in the source document :) + let $contextNode := util:node-by-id($hit, $matchId) + (: and get the pb preceding it :) + let $page := $contextNode/preceding::tei:pb[1] + return + if ($page) then + util:node-id($page) + else + util:node-id($div) + else + (: Check if the document has sections, otherwise don't pass root :) + if (nav:get-section-for-node($config, $div)) then util:node-id($div) else () + let $config := + return + kwic:get-summary($expanded, $match, $config) + } +
+
+}; + +declare function sapi:facets($request as map(*)) { + + let $hits := session:get-attribute($config:session-prefix || ".hits") + where count($hits) > 0 + return +
+ { + for $config in $facets-config:facets?* + return + facets:display($config, $hits) + } +
+}; + +declare function sapi:list-facets($request as map(*)) { + let $type := $request?parameters?type + let $lang := tokenize($request?parameters?language, '-')[1] + let $facetConfig := filter($facets-config:facets?*, function($facetCfg) { + $facetCfg?dimension = $type + }) + let $hits := session:get-attribute($config:session-prefix || ".hits") + let $facets := ft:facets($hits, $type, ()) + let $matches := + for $key in if (exists($request?parameters?value)) then $request?parameters?value else map:keys($facets) + let $label := facets:translate($facetConfig, $lang, $key) + return + map { + "text": $label, + "freq": $facets($key), + "value": $key + } + + let $filtered := filter($matches, function($item) { + matches($item?text, '(?:^|\W)' || $request?parameters?query, 'i') + }) + return + array { $filtered } +};