implemented full-text search

This commit is contained in:
Oleg Belyaev 2025-03-22 23:43:48 +03:00
parent 947fe623da
commit 9b42996cef
6 changed files with 203 additions and 15 deletions

View file

@ -89,7 +89,11 @@ body>header>nav,
body>footer>nav {
margin: auto;
--pico-primary: #8D0B02 !important;
--pico-primary-background: #8D0B02 !important;
--pico-primary-border: #8D0B02 !important;
--pico-primary-hover: #cd5c5c !important;
--pico-primary-hover-background: #cd5c5c !important;
--pico-primary-hover-border: #cd5c5c !important;
}
body>header>nav {
border-bottom-width: thin;
@ -305,6 +309,21 @@ legend {}
form {}
header form {
margin-bottom: 0 !important;
}
header form button {
padding-left: 1em !important;
padding-right: 1em !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
header form select {
max-width:35%;
}
label {}
::placeholder {}

View file

@ -45,7 +45,7 @@ $( document ).ready(function() {
if(glosses.length > 0) {
entrystring = `${glosses.join('; ')}: `
}
opt = $(`<option value="${out.xmlid}">${entrystring}<i>${out.form}</i>
opt = $(`<option value="${out.xmlid}" data-abv-xpath="${e.refs[0].path}">${entrystring}<i>${out.form}</i>
${out.glosses.map(g => `${g}`).join(', ')}</option>`);
$select_entry.append(opt)
})
@ -57,7 +57,8 @@ $( document ).ready(function() {
$('#abv-select-entry').on("dblclick", function() {
let $selected = $(this).children('option:selected');
let $entry = $selected.attr('value');
window.location.replace('./dict/' + $entry);
let entry = $selected.attr('value');
let path = $selected.attr('data-abv-xpath');
window.location.replace('./dict/' + entry + `?entry=${entry}&xpath=${path}`);
});
});

View file

@ -268,3 +268,39 @@ declare function abv-m:langname-by-id($id as xs:string, $lang as xs:string) {
declare function abv-m:entry-form-by-id($id as xs:string) {
doc(`abaevdict_index/lookup.xml`)/tei:table[1]/tei:entry[@xml:id=$id]/text()
};
declare function abv-m:mark-element($doc as document-node(), $path as xs:string) {
let $doc-tr := $doc transform with {
for $n in xquery:eval($path, {'': .})
return replace node $n
with <abv:mark>{$n}</abv:mark>
}
return $doc-tr
};
(: Function to search, used in API and elsewhere :)
declare function abv-m:search($db-lang as xs:string,
$type as xs:string,
$query as xs:string) {
let $pexpr := string-join(
('declare namespace tei = "http://www.tei-c.org/ns/1.0";',
switch($type)
case "full" return "//text()"
case "form" return "/tei:entry[1]/tei:form/tei:orth"
case "sense" return "/tei:entry[1]/tei:sense"
case "example" return "/tei:entry[1]//tei:cit[@type='example']/tei:quote"
case "translation" return "/tei:entry[1]//tei:cit[@type='translation']"
case "mentioned" return "/tei:entry[1]//tei:mentioned/(tei:m|tei:w|tei:phr|tei:s)"
case "gloss" return "tei:entry[1]//tei:gloss"
case "etym" return "tei:entry[1]/tei:etym[1]//text()"
default return "//text()")
)
return array{for $doc in collection(`abaevdict_{$db-lang}/xml`)
let $hits := for $node in xquery:eval($pexpr, {'': $doc})
where $node contains text {$query}
return path($node)
where count($hits) > 0
order by abv-m:sortKey($doc/tei:entry[1]/tei:form[1]/tei:orth[1])
return {'entry_id': string($doc/tei:entry[1]/@xml:id),
'path': array:build($hits)}}
};

View file

@ -85,7 +85,7 @@ declare function api:word-info-long($n as node()) {
'refs': array:build(./ref,
fn {
map:merge((
map:entry('node-id', string(./@node-id)),
map:entry('path', string(./@path)),
if(./gloss) then
{'glosses': array:build(distinct-values(./gloss/@text), string#1)}
else ()
@ -168,7 +168,7 @@ declare %rest:path("{$db-lang}/api/languages/{$lang}/words/{$word-id}")
)
};
(: Entry info (only form for now) :)
(: Entry info :)
declare %rest:path("{$db-lang}/api/entries")
%rest:GET
function api:entries($db-lang as xs:string) {
@ -187,4 +187,15 @@ declare %rest:path("{$db-lang}/api/entries/{$entry-id}")
$db-lang)
};
(: Search API :)
declare %rest:path("{$db-lang}/api/search")
%rest:query-param("type","{$type}",'full')
%rest:query-param("query","{$query}")
%rest:GET
function api:search($db-lang as xs:string,
$type as xs:string,
$query as xs:string) {
abv-m:search($db-lang,$type,$query)
};
0

View file

@ -4,6 +4,7 @@ declare namespace tei = "http://www.tei-c.org/ns/1.0";
import module namespace abv-m = 'http://ossetic-studies.org/ns/abaevdict-mod' at './abv-mod.xqm';
(: =========================================================== :)
(: ================== GLOBAL VARIABLES ======================= :)
(: =========================================================== :)
@ -49,7 +50,7 @@ declare function page:header($lang as xs:string, $href-other as xs:string) {
<strong>{
switch($lang)
case 'ru' return 'ИЭСОЯ В.&#160;И.&#160;Абаева'
default return 'Abaevdict'
default return `Abaevdict`
}</strong><i> β</i></li>
<li>{
(element {if ($lang = 'ru') then 'mark' else 'a'}{
@ -68,6 +69,59 @@ declare function page:header($lang as xs:string, $href-other as xs:string) {
</li>
</ul>
<ul>
<li>
<form role="search" action="./search/new" id="abv-search">
{if (session:get('searchQuery')) then
<button onclick="location.href='./search/clear'" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-lg" viewBox="0 0 16 16">
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z"/>
</svg>
</button>
else
<select name="searchType" required="1">
<option selected="1" value="full">All</option>
<option value="form">Forms</option>
<option value="sense">Senses</option>
<option value="example">Examples</option>
<option value="translation">Translations</option>
<option value="mentioned">Mentioned</option>
<option value="gloss">Glosses</option>
<option value="etym">Etymology</option>
</select>
}
<input name="searchQuery"
placeholder="{if ($lang = 'ru') then 'Искать' else 'Search'}"
value="{session:get('searchQuery')}"
aria-label="Search" />
{if (not(session:get('searchQuery'))) then
<button type="submit">
<svg xmlns="http://www.w3.org/2000/svg"
width="16" height="16"
viewBox="0 0 16 16"
fill="currentColor" class="bi bi-search">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0"/>
</svg>
</button>
else
(
if (session:get('searchN') > 1) then
<button type="button" onclick="location.href='./search/prev'">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8"/>
</svg>
</button>,
<button disabled="true">{session:get('searchN')}/{array:size(session:get('searchData'))}</button>
,
if (session:get('searchN') and session:get('searchN') < array:size(session:get('searchData'))) then
<button type="button" onclick="location.href='./search/next'">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8"/>
</svg>
</button>
)
}
</form>
</li>
<li><a href="./home">{
switch($lang)
case 'ru' return 'Главная'
@ -96,6 +150,62 @@ declare function page:invert-lang($lang as xs:string) as xs:string {
if($lang = 'en') then 'ru' else 'en'
};
(: =============================================================== :)
(: ======================= SEARCH ENTRYPOINT ===================== :)
(: =============================================================== :)
declare %rest:path("/search/{$path=.+}")
%rest:GET
function page:search-default($path as xs:string) {
web:forward(`/en/search/{$path}`)
};
declare %rest:path("{$lang}/search/new")
%rest:query-param("searchType", "{$searchType}", 'full')
%rest:query-param("searchQuery", "{$searchQuery}")
%output:method("html")
%output:html-version('5')
function page:search($lang, $searchType, $searchQuery) {
let $r1 := session:set('searchType', $searchType)
let $r2 := session:set('searchQuery', $searchQuery)
let $r3 := session:set('searchN', 1)
let $sd := abv-m:search($lang,$searchType,$searchQuery)
let $r4 := session:set('searchData',
$sd)
return page:by-id($lang, $sd(1)('entry_id'), string-join($sd(1)('path')?*,"|"))
};
declare %rest:path("{$lang}/search/next")
%output:method("html")
%output:html-version('5')
function page:search-next($lang) {
let $n := session:get('searchN')+1
let $r1 := session:set('searchN', $n)
let $sd := session:get('searchData')
return page:by-id($lang, $sd($n)('entry_id'), string-join($sd($n)('path')?*,"|"))
};
declare %rest:path("{$lang}/search/prev")
%output:method("html")
%output:html-version('5')
function page:search-prev($lang) {
let $n := session:get('searchN')-1
let $r1 := session:set('searchN', $n)
let $sd := session:get('searchData')
return page:by-id($lang, $sd($n)('entry_id'), string-join($sd($n)('path')?*,"|"))
};
declare %rest:path("{$lang}/search/clear")
%output:method("html")
%output:html-version('5')
function page:clearSearch($lang) {
let $r1 := session:delete('searchType')
let $r2 := session:delete('searchQuery')
let $r3 := session:delete('searchN')
let $r4 := session:delete('searchData')
return web:redirect('../dict')
};
(: =============================================================== :)
(: ======================= HOMEPAGE ============================== :)
(: =============================================================== :)
@ -328,9 +438,11 @@ declare %rest:path("dict")
(: The main dictionary view :)
declare %rest:path("{$lang}/dict")
%rest:query-param("page","{$p}", 1)
%rest:query-param("xpath","{$xpath}", '')
%rest:query-param("entry","{$entry}", '')
%output:method("html")
%output:html-version('5')
function page:dict($lang, $p) {
function page:dict($lang, $p, $xpath, $entry) {
<html>
{page:head('HEDO Dictionary',
(<script src="/static/infinite-scroll.pkgd.min.js">
@ -381,7 +493,9 @@ declare %rest:path("{$lang}/dict")
<main>
{for $doc at $i in $page:sorted
where $i > ($p - 1) * $page:items and $i <= $p * $page:items
let $html := doc(`abaevdict_{$lang}/html/{$doc/@xml:id}.html`)
let $html := if ($xpath != '' and $entry = $doc/@xml:id)
then abv-m:mark-element(doc(`abaevdict_{$lang}/xml/{$doc/@xml:id}.xml`),$xpath) => abv-m:make-html($lang)
else doc(`abaevdict_{$lang}/html/{$doc/@xml:id}.html`)
return if (doc('abaevdict_index/mentioned_en.xml')/
lang-index/lang[@id != 'os' and
not(starts-with(@id,'os-'))]/word/entry[@id=string($doc/@xml:id)]) then
@ -430,20 +544,26 @@ declare %rest:path("{$lang}/dict")
(: == ENTRY BY ID == :)
(: English as default language :)
declare %rest:path("dict/{$id}")
%rest:query-param("xpath", "{$xpath}")
%output:method("html")
%output:html-version('5')
function page:by-id($id) {
page:by-id('en',$id)
function page:by-id($id, $xpath) {
page:by-id('en', $id, $xpath)
};
declare %rest:path("{$lang}/dict/{$id}")
%rest:query-param("xpath", "{$xpath}")
%output:method("html")
%output:html-version('5')
function page:by-id($lang, $id) {
function page:by-id($lang, $id, $xpath) {
(: let $doc-index := index-of($page:sorted, $page:sorted[@xml:id=`{$id}`]) :)
let $doc-index := index-where($page:sorted, fn { ./@xml:id=$id })
let $pagenum := ceiling($doc-index div $page:items)
return web:redirect('../dict', {'page': $pagenum}, web:decode-url($id))
return web:redirect('../dict',
{'page': $pagenum,
'entry': web:decode-url($id),
'xpath': $xpath},
web:decode-url($id))
};
(: =============================================================== :)

View file

@ -26,7 +26,7 @@
<xsl:template match="abv:mark">
<mark>
<xsl:next-match/>
<xsl:apply-templates/>
</mark>
</xsl:template>
@ -129,7 +129,8 @@
</xsl:element>
</xsl:template>
<xsl:template match="tei:cit[@type='example']/tei:quote">
<xsl:template match="tei:cit[@type='example']/tei:quote |
tei:cit[@type='example']/abv:mark/tei:quote">
<xsl:element name="i">
<xsl:attribute name="class">abv-example</xsl:attribute>
<xsl:attribute name="lang" select="../@xml:lang"/>