mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 02:34:06 -04:00
More work on the FTS search page
This commit is contained in:
parent
4b873f1941
commit
cd1c550cde
@ -50,6 +50,24 @@ class HTTPBadRequest(HTTPSimpleResponse):
|
|||||||
HTTPSimpleResponse.__init__(self, http_client.BAD_REQUEST, message, close_connection)
|
HTTPSimpleResponse.__init__(self, http_client.BAD_REQUEST, message, close_connection)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPFailedDependency(HTTPSimpleResponse):
|
||||||
|
|
||||||
|
def __init__(self, message, close_connection=False):
|
||||||
|
HTTPSimpleResponse.__init__(self, http_client.FAILED_DEPENDENCY, message, close_connection)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPPreconditionRequired(HTTPSimpleResponse):
|
||||||
|
|
||||||
|
def __init__(self, message, close_connection=False):
|
||||||
|
HTTPSimpleResponse.__init__(self, http_client.PRECONDITION_REQUIRED, message, close_connection)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPUnprocessableEntity(HTTPSimpleResponse):
|
||||||
|
|
||||||
|
def __init__(self, message, close_connection=False):
|
||||||
|
HTTPSimpleResponse.__init__(self, http_client.UNPROCESSABLE_ENTITY, message, close_connection)
|
||||||
|
|
||||||
|
|
||||||
class HTTPForbidden(HTTPSimpleResponse):
|
class HTTPForbidden(HTTPSimpleResponse):
|
||||||
|
|
||||||
def __init__(self, http_message='', close_connection=True, log=None):
|
def __init__(self, http_message='', close_connection=True, log=None):
|
||||||
|
51
src/calibre/srv/fts.py
Normal file
51
src/calibre/srv/fts.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPL v3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
from calibre.srv.errors import (
|
||||||
|
HTTPBadRequest, HTTPPreconditionRequired, HTTPUnprocessableEntity,
|
||||||
|
)
|
||||||
|
from calibre.srv.routes import endpoint, json
|
||||||
|
from calibre.srv.utils import get_library_data
|
||||||
|
|
||||||
|
|
||||||
|
@endpoint('/fts/search', postprocess=json)
|
||||||
|
def fts_search(ctx, rd):
|
||||||
|
'''
|
||||||
|
Perform the specified full text query.
|
||||||
|
|
||||||
|
Optional: ?query=<search query>&library_id=<default library>&use_stemming=<y or n>&query_id=arbitrary
|
||||||
|
'''
|
||||||
|
|
||||||
|
db = get_library_data(ctx, rd)[0]
|
||||||
|
if not db.is_fts_enabled():
|
||||||
|
raise HTTPPreconditionRequired('Full text searching is not enabled on this library')
|
||||||
|
metadata_cache = {}
|
||||||
|
l, t = db.fts_indexing_progress()[:2]
|
||||||
|
ans = {'metadata': metadata_cache, 'left': l, 'total': t}
|
||||||
|
|
||||||
|
use_stemming = rd.query.get('use_stemming', 'y') == 'y'
|
||||||
|
query = rd.query.get('query' '')
|
||||||
|
if not query:
|
||||||
|
raise HTTPBadRequest('No search query specified')
|
||||||
|
qid = rd.query.get('query_id')
|
||||||
|
if qid:
|
||||||
|
ans['query_id'] = qid
|
||||||
|
|
||||||
|
def add_metadata(result):
|
||||||
|
result.pop('id', None)
|
||||||
|
result.pop('text', None)
|
||||||
|
bid = result['book_id']
|
||||||
|
if bid not in metadata_cache:
|
||||||
|
with db.safe_read_lock:
|
||||||
|
metadata_cache[bid] = {'title': db._field_for('title', bid), 'authors': db._field_for('authors', bid)}
|
||||||
|
return result
|
||||||
|
|
||||||
|
from calibre.db import FTSQueryError
|
||||||
|
try:
|
||||||
|
ans['results'] = tuple(db.fts_search(
|
||||||
|
query, use_stemming=use_stemming, return_text=False, result_type=lambda x: x, process_each_result=add_metadata,
|
||||||
|
))
|
||||||
|
except FTSQueryError as e:
|
||||||
|
raise HTTPUnprocessableEntity(str(e))
|
||||||
|
return ans
|
@ -182,7 +182,7 @@ class Context:
|
|||||||
return old[1]
|
return old[1]
|
||||||
|
|
||||||
|
|
||||||
SRV_MODULES = ('ajax', 'books', 'cdb', 'code', 'content', 'legacy', 'opds', 'users_api', 'convert')
|
SRV_MODULES = ('ajax', 'books', 'cdb', 'code', 'content', 'legacy', 'opds', 'users_api', 'convert', 'fts')
|
||||||
|
|
||||||
|
|
||||||
class Handler:
|
class Handler:
|
||||||
|
@ -4,20 +4,24 @@ from __python__ import bound_methods, hash_literals
|
|||||||
|
|
||||||
from elementmaker import E
|
from elementmaker import E
|
||||||
|
|
||||||
from book_list.globals import get_session_data
|
from ajax import ajax
|
||||||
from book_list.router import back
|
from book_list.globals import get_session_data, get_current_query
|
||||||
|
from book_list.router import back, push_state
|
||||||
from book_list.top_bar import create_top_bar
|
from book_list.top_bar import create_top_bar
|
||||||
from book_list.ui import set_panel_handler
|
from book_list.ui import set_panel_handler
|
||||||
from complete import create_search_bar
|
from complete import create_search_bar
|
||||||
from dom import add_extra_css, clear, set_css
|
from dom import add_extra_css, clear, set_css
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from widgets import create_button
|
from modals import error_dialog
|
||||||
|
from widgets import create_button, create_spinner
|
||||||
|
|
||||||
overall_container_id = ''
|
overall_container_id = ''
|
||||||
|
current_fts_query = {}
|
||||||
|
query_id_counter = 0
|
||||||
|
|
||||||
add_extra_css(def():
|
add_extra_css(def():
|
||||||
sel = '.fts-help-display '
|
sel = '.fts-help-display '
|
||||||
style = f'{sel} ' + '{ margin-left: 1em; padding-top: 0.5ex }\n'
|
style = f'{sel} ' + '{ padding-top: 0.5ex }\n'
|
||||||
style += f'{sel} div' + ' { margin-top: 0.5ex }\n'
|
style += f'{sel} div' + ' { margin-top: 0.5ex }\n'
|
||||||
style += f'{sel} .h' + ' { font-weight: bold; padding-bottom: 0.25ex }\n'
|
style += f'{sel} .h' + ' { font-weight: bold; padding-bottom: 0.25ex }\n'
|
||||||
style += f'{sel} .bq' + ' { margin-left: 1em; margin-top: 0.5ex; margin-bottom: 0.5ex; font-style: italic }\n'
|
style += f'{sel} .bq' + ' { margin-left: 1em; margin-top: 0.5ex; margin-bottom: 0.5ex; font-style: italic }\n'
|
||||||
@ -26,11 +30,53 @@ add_extra_css(def():
|
|||||||
)
|
)
|
||||||
|
|
||||||
def component(name):
|
def component(name):
|
||||||
return document.getElementById(overall_container_id).querySelector(f'[data-component="{name}"]')
|
return document.getElementById(overall_container_id)?.querySelector(f'[data-component="{name}"]')
|
||||||
|
|
||||||
|
|
||||||
|
def showing_search_panel():
|
||||||
|
c = component('search')
|
||||||
|
return bool(c and c.style.display is 'block')
|
||||||
|
|
||||||
|
|
||||||
|
def make_new_fts_query(q):
|
||||||
|
nonlocal current_fts_query, query_id_counter
|
||||||
|
query_id_counter += 1
|
||||||
|
current_fts_query = {'query_id': query_id_counter}
|
||||||
|
Object.assign(current_fts_query, q)
|
||||||
|
xhr = ajax('fts/search', on_initial_fts_fetched, query=current_fts_query, bypass_cache=True)
|
||||||
|
xhr.send()
|
||||||
|
|
||||||
|
|
||||||
|
def on_initial_fts_fetched(end_type, xhr, ev):
|
||||||
|
if end_type is 'abort' or not showing_search_panel():
|
||||||
|
return
|
||||||
|
if end_type is not 'load':
|
||||||
|
if xhr.status is 428:
|
||||||
|
# TODO: Implement FTS not enabled
|
||||||
|
pass
|
||||||
|
return error_dialog(_('Failed to search'), _('The search failed. Click "Show details" for more information.'), xhr.error_html)
|
||||||
|
try:
|
||||||
|
results = JSON.parse(xhr.responseText)
|
||||||
|
except Exception as err:
|
||||||
|
return error_dialog(_('Server error'), _('Failed to parse search response from server.'), err + '')
|
||||||
|
if results.query_id is not current_fts_query.query_id:
|
||||||
|
return
|
||||||
|
current_fts_query.results = results
|
||||||
|
show_initial_results()
|
||||||
|
|
||||||
|
|
||||||
def execute_search_interactive():
|
def execute_search_interactive():
|
||||||
pass
|
nonlocal current_fts_query
|
||||||
|
query = component('query').querySelector('input').value
|
||||||
|
if not query or query.length < 1:
|
||||||
|
error_dialog(_('No search query specified'), _('A search word/phrase must be specified before attempting to search'))
|
||||||
|
return
|
||||||
|
q = get_current_query()
|
||||||
|
q.fts_panel = 'search'
|
||||||
|
q.fts_query = query
|
||||||
|
q.fts_use_stemming = 'y' if component('related_words').checked else 'n'
|
||||||
|
current_fts_query = {}
|
||||||
|
push_state(q, replace=True)
|
||||||
|
|
||||||
|
|
||||||
def build_search_page():
|
def build_search_page():
|
||||||
@ -40,23 +86,44 @@ def build_search_page():
|
|||||||
search_button = create_button(_('Search'), icon='search', tooltip=_('Do the search'))
|
search_button = create_button(_('Search'), icon='search', tooltip=_('Do the search'))
|
||||||
search_bar = create_search_bar(execute_search_interactive, 'search-books-fts', tooltip=_('Search for books'), placeholder=_('Enter words to search for'), button=search_button)
|
search_bar = create_search_bar(execute_search_interactive, 'search-books-fts', tooltip=_('Search for books'), placeholder=_('Enter words to search for'), button=search_button)
|
||||||
set_css(search_bar, flex_grow='10', margin_right='0.5em')
|
set_css(search_bar, flex_grow='10', margin_right='0.5em')
|
||||||
|
search_bar.dataset.component = 'query'
|
||||||
container.appendChild(E.div(style="display: flex; width: 100%;", search_bar, search_button))
|
container.appendChild(E.div(style="display: flex; width: 100%;", search_bar, search_button))
|
||||||
sd = get_session_data()
|
sd = get_session_data()
|
||||||
related_words = E.label(E.input(
|
related_words = E.label(E.input(
|
||||||
type="checkbox", data_component="related_words", checked=bool(sd.get('fts_related_words'))),
|
type="checkbox", data_component="related_words", checked=bool(sd.get('fts_related_words'))),
|
||||||
onchange=def():
|
onchange=def():
|
||||||
get_session_data().set('fts_related_words', bool(component('related_words').checked))
|
sd = get_session_data()
|
||||||
|
rw = bool(component('related_words').checked)
|
||||||
|
if rw is not sd.get('fts_related_words'):
|
||||||
|
get_session_data().set('fts_related_words', rw)
|
||||||
, ' ' + _('Match on related words'),
|
, ' ' + _('Match on related words'),
|
||||||
title=_(
|
title=_(
|
||||||
'With this option searching for words will also match on any related words (supported in several languages). For'
|
'With this option searching for words will also match on any related words (supported in several languages). For'
|
||||||
' example, in the English language: {0} matches {1} and {2} as well').format(
|
' example, in the English language: {0} matches {1} and {2} as well').format(
|
||||||
'correction', 'correcting', 'corrected')
|
'correction', 'correcting', 'corrected')
|
||||||
)
|
)
|
||||||
container.appendChild(E.div(style="text-align:left; padding-top: 1ex", related_words))
|
container.appendChild(E.div(style="padding-top: 1ex; border-bottom: solid 1px currentColor; width: 100%", related_words))
|
||||||
|
|
||||||
|
# Search help
|
||||||
|
container.appendChild(E.div(data_component='results'))
|
||||||
|
|
||||||
|
|
||||||
def show_search_help():
|
def clear_to_waiting_for_results(msg):
|
||||||
container = component('results')
|
container = component('results')
|
||||||
|
if not container:
|
||||||
|
return
|
||||||
|
clear(container)
|
||||||
|
msg = msg or (_('Searching, please wait') + '…')
|
||||||
|
container.appendChild(E.div(
|
||||||
|
style='margin: auto; width: 100%; text-align: center; margin-top: 4ex',
|
||||||
|
create_spinner(), '\xa0' + msg
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def clear_to_help():
|
||||||
|
container = component('results')
|
||||||
|
if not container:
|
||||||
|
return
|
||||||
clear(container)
|
clear(container)
|
||||||
container.appendChild(E.div(class_='fts-help-display'))
|
container.appendChild(E.div(class_='fts-help-display'))
|
||||||
container = container.firstChild
|
container = container.firstChild
|
||||||
@ -85,19 +152,64 @@ def show_search_help():
|
|||||||
a.classList.add('blue-link')
|
a.classList.add('blue-link')
|
||||||
|
|
||||||
|
|
||||||
|
def apply_search_panel_state():
|
||||||
|
q = get_current_query()
|
||||||
|
ftsq = {'query': q.fts_query, 'use_stemming': q.fts_use_stemming}
|
||||||
|
component('query').querySelector('input').value = ftsq.query
|
||||||
|
component('related_words').checked = ftsq.use_stemming is 'y'
|
||||||
|
if not ftsq.query:
|
||||||
|
clear_to_help()
|
||||||
|
return
|
||||||
|
if current_fts_query.query is not ftsq.query or current_fts_query.use_stemming is not ftsq.use_stemming:
|
||||||
|
make_new_fts_query(ftsq)
|
||||||
|
clear_to_waiting_for_results(_('Searching for {}, please wait…').format(ftsq.query))
|
||||||
|
return
|
||||||
|
show_initial_results()
|
||||||
|
|
||||||
|
|
||||||
|
def show_initial_results():
|
||||||
|
container = component('results')
|
||||||
|
if not container:
|
||||||
|
return
|
||||||
|
clear(container)
|
||||||
|
results = current_fts_query.results
|
||||||
|
results
|
||||||
|
|
||||||
|
|
||||||
|
def show_panel(visible, hidden):
|
||||||
|
c = component(visible)
|
||||||
|
if c:
|
||||||
|
c.style.display = 'block'
|
||||||
|
c = component(hidden)
|
||||||
|
if c:
|
||||||
|
c.style.display = 'none'
|
||||||
|
|
||||||
|
|
||||||
|
def show_search_panel():
|
||||||
|
show_panel('search', 'index')
|
||||||
|
apply_search_panel_state()
|
||||||
|
|
||||||
|
|
||||||
|
def show_index_panel():
|
||||||
|
show_panel('index', 'search')
|
||||||
|
|
||||||
|
|
||||||
def init(container_id):
|
def init(container_id):
|
||||||
nonlocal overall_container_id
|
nonlocal overall_container_id
|
||||||
overall_container_id = container_id
|
overall_container_id = container_id
|
||||||
container = document.getElementById(container_id)
|
container = document.getElementById(container_id)
|
||||||
create_top_bar(container, title=_('Search text of books'), action=back, icon='close')
|
create_top_bar(container, title=_('Search text of books'), action=back, icon='close')
|
||||||
container.appendChild(E.div(data_component='indexing'))
|
container.appendChild(E.div(data_component='index'))
|
||||||
container.appendChild(E.div(
|
container.appendChild(E.div(
|
||||||
data_component='search',
|
data_component='search',
|
||||||
style="text-align:center; padding:1ex 1em; border-bottom: solid 1px currentColor; margin-top: 0.5ex; padding-bottom: 1.5ex; margin-bottom: 0.5ex"
|
style="padding:1ex 1em; margin-top: 0.5ex;"
|
||||||
))
|
))
|
||||||
container.appendChild(E.div(data_component='results'))
|
|
||||||
build_search_page()
|
build_search_page()
|
||||||
show_search_help()
|
q = get_current_query()
|
||||||
|
if not q.fts_panel or q.fts_panel is 'search':
|
||||||
|
show_search_panel()
|
||||||
|
elif q.fts_panel is 'index':
|
||||||
|
show_index_panel()
|
||||||
|
|
||||||
|
|
||||||
set_panel_handler('fts', init)
|
set_panel_handler('fts', init)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user