mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 18:24:30 -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)
|
||||
|
||||
|
||||
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):
|
||||
|
||||
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]
|
||||
|
||||
|
||||
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:
|
||||
|
@ -4,20 +4,24 @@ from __python__ import bound_methods, hash_literals
|
||||
|
||||
from elementmaker import E
|
||||
|
||||
from book_list.globals import get_session_data
|
||||
from book_list.router import back
|
||||
from ajax import ajax
|
||||
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.ui import set_panel_handler
|
||||
from complete import create_search_bar
|
||||
from dom import add_extra_css, clear, set_css
|
||||
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 = ''
|
||||
current_fts_query = {}
|
||||
query_id_counter = 0
|
||||
|
||||
add_extra_css(def():
|
||||
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} .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'
|
||||
@ -26,11 +30,53 @@ add_extra_css(def():
|
||||
)
|
||||
|
||||
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():
|
||||
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():
|
||||
@ -40,23 +86,44 @@ def build_search_page():
|
||||
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)
|
||||
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))
|
||||
sd = get_session_data()
|
||||
related_words = E.label(E.input(
|
||||
type="checkbox", data_component="related_words", checked=bool(sd.get('fts_related_words'))),
|
||||
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'),
|
||||
title=_(
|
||||
'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(
|
||||
'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')
|
||||
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)
|
||||
container.appendChild(E.div(class_='fts-help-display'))
|
||||
container = container.firstChild
|
||||
@ -85,19 +152,64 @@ def show_search_help():
|
||||
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):
|
||||
nonlocal overall_container_id
|
||||
overall_container_id = container_id
|
||||
container = document.getElementById(container_id)
|
||||
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(
|
||||
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()
|
||||
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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user