[mod] migrate all key-value.html templates to KeyValue type

The engines now all use KeyValue results and return the results in a
EngineResults object.

The sqlite engine can return MainResult results in addition to KeyValue
results (based on engine's config in settings.yml),

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
This commit is contained in:
Markus Heiser 2025-03-05 17:50:22 +01:00 committed by Markus Heiser
parent af5dbdf768
commit f49b2c94a9
13 changed files with 160 additions and 172 deletions

View File

@ -81,6 +81,7 @@ from subprocess import Popen, PIPE
from threading import Thread from threading import Thread
from searx import logger from searx import logger
from searx.result_types import EngineResults
engine_type = 'offline' engine_type = 'offline'
@ -93,7 +94,6 @@ query_enum = []
environment_variables = {} environment_variables = {}
working_dir = realpath('.') working_dir = realpath('.')
result_separator = '\n' result_separator = '\n'
result_template = 'key-value.html'
timeout = 4.0 timeout = 4.0
_command_logger = logger.getChild('command') _command_logger = logger.getChild('command')
@ -126,17 +126,17 @@ def init(engine_settings):
environment_variables = engine_settings['environment_variables'] environment_variables = engine_settings['environment_variables']
def search(query, params): def search(query, params) -> EngineResults:
res = EngineResults()
cmd = _get_command_to_run(query) cmd = _get_command_to_run(query)
if not cmd: if not cmd:
return [] return res
results = [] reader_thread = Thread(target=_get_results_from_process, args=(res, cmd, params['pageno']))
reader_thread = Thread(target=_get_results_from_process, args=(results, cmd, params['pageno']))
reader_thread.start() reader_thread.start()
reader_thread.join(timeout=timeout) reader_thread.join(timeout=timeout)
return results return res
def _get_command_to_run(query): def _get_command_to_run(query):
@ -153,7 +153,7 @@ def _get_command_to_run(query):
return cmd return cmd
def _get_results_from_process(results, cmd, pageno): def _get_results_from_process(res: EngineResults, cmd, pageno):
leftover = '' leftover = ''
count = 0 count = 0
start, end = __get_results_limits(pageno) start, end = __get_results_limits(pageno)
@ -173,12 +173,11 @@ def _get_results_from_process(results, cmd, pageno):
continue continue
if start <= count and count <= end: # pylint: disable=chained-comparison if start <= count and count <= end: # pylint: disable=chained-comparison
result['template'] = result_template res.add(res.types.KeyValue(kvmap=result))
results.append(result)
count += 1 count += 1
if end < count: if end < count:
return results return res
line = process.stdout.readline() line = process.stdout.readline()

View File

@ -13,6 +13,7 @@ close to the implementation, its just a simple example. To get in use of this
""" """
import json import json
from searx.result_types import EngineResults from searx.result_types import EngineResults
engine_type = 'offline' engine_type = 'offline'
@ -29,7 +30,7 @@ about = {
} }
# if there is a need for globals, use a leading underline # if there is a need for globals, use a leading underline
_my_offline_engine = None _my_offline_engine: str = ""
def init(engine_settings=None): def init(engine_settings=None):
@ -50,24 +51,28 @@ def init(engine_settings=None):
def search(query, request_params) -> EngineResults: def search(query, request_params) -> EngineResults:
"""Query (offline) engine and return results. Assemble the list of results from """Query (offline) engine and return results. Assemble the list of results
your local engine. In this demo engine we ignore the 'query' term, usual from your local engine. In this demo engine we ignore the 'query' term,
you would pass the 'query' term to your local engine to filter out the usual you would pass the 'query' term to your local engine to filter out the
results. results.
""" """
res = EngineResults() res = EngineResults()
result_list = json.loads(_my_offline_engine) count = 0
for row in json.loads(_my_offline_engine):
for row in result_list: count += 1
entry = { kvmap = {
'query': query, 'query': query,
'language': request_params['searxng_locale'], 'language': request_params['searxng_locale'],
'value': row.get("value"), 'value': row.get("value"),
# choose a result template or comment out to use the *default*
'template': 'key-value.html',
} }
res.append(entry) res.add(
res.types.KeyValue(
caption=f"Demo Offline Engine Result #{count}",
key_title="Name",
value_title="Value",
kvmap=kvmap,
)
)
res.add(res.types.LegacyResult(number_of_results=count))
return res return res

View File

@ -43,6 +43,8 @@ authentication configured to read from ``my-index`` index.
from json import loads, dumps from json import loads, dumps
from searx.exceptions import SearxEngineAPIException from searx.exceptions import SearxEngineAPIException
from searx.result_types import EngineResults
from searx.extended_types import SXNG_Response
base_url = 'http://localhost:9200' base_url = 'http://localhost:9200'
@ -145,23 +147,20 @@ def _custom_query(query):
return custom_query return custom_query
def response(resp): def response(resp: SXNG_Response) -> EngineResults:
results = [] res = EngineResults()
resp_json = loads(resp.text) resp_json = loads(resp.text)
if 'error' in resp_json: if 'error' in resp_json:
raise SearxEngineAPIException(resp_json['error']) raise SearxEngineAPIException(resp_json["error"])
for result in resp_json['hits']['hits']:
r = {key: str(value) if not key.startswith('_') else value for key, value in result['_source'].items()}
r['template'] = 'key-value.html'
for result in resp_json["hits"]["hits"]:
kvmap = {key: str(value) if not key.startswith("_") else value for key, value in result["_source"].items()}
if show_metadata: if show_metadata:
r['metadata'] = {'index': result['_index'], 'id': result['_id'], 'score': result['_score']} kvmap["metadata"] = {"index": result["_index"], "id": result["_id"], "score": result["_score"]}
res.add(res.types.KeyValue(kvmap=kvmap))
results.append(r) return res
return results
_available_query_types = { _available_query_types = {

View File

@ -35,6 +35,8 @@ except ImportError:
# the engine # the engine
pass pass
from searx.result_types import EngineResults
if TYPE_CHECKING: if TYPE_CHECKING:
import logging import logging
@ -63,7 +65,6 @@ query_str = ""
limit = 10 limit = 10
paging = True paging = True
result_template = 'key-value.html'
_connection = None _connection = None
@ -79,17 +80,16 @@ def init(engine_settings):
_connection = mariadb.connect(database=database, user=username, password=password, host=host, port=port) _connection = mariadb.connect(database=database, user=username, password=password, host=host, port=port)
def search(query, params): def search(query, params) -> EngineResults:
query_params = {'query': query} query_params = {'query': query}
query_to_run = query_str + ' LIMIT {0} OFFSET {1}'.format(limit, (params['pageno'] - 1) * limit) query_to_run = query_str + ' LIMIT {0} OFFSET {1}'.format(limit, (params['pageno'] - 1) * limit)
logger.debug("SQL Query: %s", query_to_run) logger.debug("SQL Query: %s", query_to_run)
res = EngineResults()
with _connection.cursor() as cur: with _connection.cursor() as cur:
cur.execute(query_to_run, query_params) cur.execute(query_to_run, query_params)
results = []
col_names = [i[0] for i in cur.description] col_names = [i[0] for i in cur.description]
for res in cur: for row in cur:
result = dict(zip(col_names, map(str, res))) kvmap = dict(zip(col_names, map(str, row)))
result['template'] = result_template res.add(res.types.KeyValue(kvmap=kvmap))
results.append(result) return res
return results

View File

@ -33,15 +33,15 @@ Here is a simple example to query a Meilisearch instance:
# pylint: disable=global-statement # pylint: disable=global-statement
from json import loads, dumps from json import dumps
from searx.result_types import EngineResults
from searx.extended_types import SXNG_Response
base_url = 'http://localhost:7700' base_url = 'http://localhost:7700'
index = '' index = ''
auth_key = '' auth_key = ''
facet_filters = [] facet_filters = []
_search_url = '' _search_url = ''
result_template = 'key-value.html'
categories = ['general'] categories = ['general']
paging = True paging = True
@ -75,13 +75,12 @@ def request(query, params):
return params return params
def response(resp): def response(resp: SXNG_Response) -> EngineResults:
results = [] res = EngineResults()
resp_json = loads(resp.text) resp_json = resp.json()
for result in resp_json['hits']: for row in resp_json['hits']:
r = {key: str(value) for key, value in result.items()} kvmap = {key: str(value) for key, value in row.items()}
r['template'] = result_template res.add(res.types.KeyValue(kvmap=kvmap))
results.append(r)
return results return res

View File

@ -37,6 +37,7 @@ Implementations
=============== ===============
""" """
from __future__ import annotations
import re import re
@ -47,6 +48,8 @@ except ImportError:
# to use the engine # to use the engine
pass pass
from searx.result_types import EngineResults
engine_type = 'offline' engine_type = 'offline'
@ -63,7 +66,6 @@ key = None
paging = True paging = True
results_per_page = 20 results_per_page = 20
exact_match_only = False exact_match_only = False
result_template = 'key-value.html'
_client = None _client = None
@ -74,7 +76,7 @@ def init(_):
def connect(): def connect():
global _client # pylint: disable=global-statement global _client # pylint: disable=global-statement
kwargs = {'port': port} kwargs: dict[str, str | int] = {'port': port}
if username: if username:
kwargs['username'] = username kwargs['username'] = username
if password: if password:
@ -82,8 +84,8 @@ def connect():
_client = MongoClient(host, **kwargs)[database][collection] _client = MongoClient(host, **kwargs)[database][collection]
def search(query, params): def search(query, params) -> EngineResults:
results = [] res = EngineResults()
if exact_match_only: if exact_match_only:
q = {'$eq': query} q = {'$eq': query}
else: else:
@ -92,11 +94,10 @@ def search(query, params):
query = _client.find({key: q}).skip((params['pageno'] - 1) * results_per_page).limit(results_per_page) query = _client.find({key: q}).skip((params['pageno'] - 1) * results_per_page).limit(results_per_page)
results.append({'number_of_results': query.count()}) res.add(res.types.LegacyResult(number_of_results=query.count()))
for r in query: for row in query:
del r['_id'] del row['_id']
r = {str(k): str(v) for k, v in r.items()} kvmap = {str(k): str(v) for k, v in row.items()}
r['template'] = result_template res.add(res.types.KeyValue(kvmap=kvmap))
results.append(r)
return results return res

View File

@ -25,6 +25,8 @@ Implementations
""" """
from searx.result_types import EngineResults
try: try:
import mysql.connector # type: ignore import mysql.connector # type: ignore
except ImportError: except ImportError:
@ -55,7 +57,6 @@ query_str = ""
limit = 10 limit = 10
paging = True paging = True
result_template = 'key-value.html'
_connection = None _connection = None
@ -78,21 +79,15 @@ def init(engine_settings):
) )
def search(query, params): def search(query, params) -> EngineResults:
res = EngineResults()
query_params = {'query': query} query_params = {'query': query}
query_to_run = query_str + ' LIMIT {0} OFFSET {1}'.format(limit, (params['pageno'] - 1) * limit) query_to_run = query_str + ' LIMIT {0} OFFSET {1}'.format(limit, (params['pageno'] - 1) * limit)
with _connection.cursor() as cur: with _connection.cursor() as cur:
cur.execute(query_to_run, query_params) cur.execute(query_to_run, query_params)
for row in cur:
kvmap = dict(zip(cur.column_names, map(str, row)))
res.add(res.types.KeyValue(kvmap=kvmap))
return _fetch_results(cur) return res
def _fetch_results(cur):
results = []
for res in cur:
result = dict(zip(cur.column_names, map(str, res)))
result['template'] = result_template
results.append(result)
return results

View File

@ -28,6 +28,8 @@ except ImportError:
# manually to use the engine. # manually to use the engine.
pass pass
from searx.result_types import EngineResults
engine_type = 'offline' engine_type = 'offline'
host = "127.0.0.1" host = "127.0.0.1"
@ -50,7 +52,6 @@ query_str = ""
limit = 10 limit = 10
paging = True paging = True
result_template = 'key-value.html'
_connection = None _connection = None
@ -72,7 +73,7 @@ def init(engine_settings):
) )
def search(query, params): def search(query, params) -> EngineResults:
query_params = {'query': query} query_params = {'query': query}
query_to_run = query_str + ' LIMIT {0} OFFSET {1}'.format(limit, (params['pageno'] - 1) * limit) query_to_run = query_str + ' LIMIT {0} OFFSET {1}'.format(limit, (params['pageno'] - 1) * limit)
@ -82,20 +83,16 @@ def search(query, params):
return _fetch_results(cur) return _fetch_results(cur)
def _fetch_results(cur): def _fetch_results(cur) -> EngineResults:
results = [] res = EngineResults()
titles = []
try: try:
titles = [column_desc.name for column_desc in cur.description] titles = [column_desc.name for column_desc in cur.description]
for row in cur:
for res in cur: kvmap = dict(zip(titles, map(str, row)))
result = dict(zip(titles, map(str, res))) res.add(res.types.KeyValue(kvmap=kvmap))
result['template'] = result_template
results.append(result)
# no results to fetch # no results to fetch
except psycopg2.ProgrammingError: except psycopg2.ProgrammingError:
pass pass
return results return res

View File

@ -36,6 +36,8 @@ Implementations
import redis # pylint: disable=import-error import redis # pylint: disable=import-error
from searx.result_types import EngineResults
engine_type = 'offline' engine_type = 'offline'
# redis connection variables # redis connection variables
@ -46,7 +48,6 @@ db = 0
# engine specific variables # engine specific variables
paging = False paging = False
result_template = 'key-value.html'
exact_match_only = True exact_match_only = True
_redis_client = None _redis_client = None
@ -63,30 +64,25 @@ def init(_engine_settings):
) )
def search(query, _params): def search(query, _params) -> EngineResults:
res = EngineResults()
if not exact_match_only: if not exact_match_only:
return search_keys(query) for kvmap in search_keys(query):
res.add(res.types.KeyValue(kvmap=kvmap))
return res
ret = _redis_client.hgetall(query) kvmap: dict[str, str] = _redis_client.hgetall(query)
if ret: if kvmap:
ret['template'] = result_template res.add(res.types.KeyValue(kvmap=kvmap))
return [ret] elif " " in query:
qset, rest = query.split(" ", 1)
if ' ' in query: for row in _redis_client.hscan_iter(qset, match='*{}*'.format(rest)):
qset, rest = query.split(' ', 1) res.add(res.types.KeyValue(kvmap={row[0]: row[1]}))
ret = [] return res
for res in _redis_client.hscan_iter(qset, match='*{}*'.format(rest)):
ret.append(
{
res[0]: res[1],
'template': result_template,
}
)
return ret
return []
def search_keys(query): def search_keys(query) -> list[dict]:
ret = [] ret = []
for key in _redis_client.scan_iter(match='*{}*'.format(query)): for key in _redis_client.scan_iter(match='*{}*'.format(query)):
key_type = _redis_client.type(key) key_type = _redis_client.type(key)
@ -98,7 +94,6 @@ def search_keys(query):
res = dict(enumerate(_redis_client.lrange(key, 0, -1))) res = dict(enumerate(_redis_client.lrange(key, 0, -1)))
if res: if res:
res['template'] = result_template
res['redis_key'] = key res['redis_key'] = key
ret.append(res) ret.append(res)
return ret return ret

View File

@ -29,9 +29,10 @@ This is an example configuration for searching in the collection
# pylint: disable=global-statement # pylint: disable=global-statement
from json import loads
from urllib.parse import urlencode from urllib.parse import urlencode
from searx.exceptions import SearxEngineAPIException from searx.exceptions import SearxEngineAPIException
from searx.result_types import EngineResults
from searx.extended_types import SXNG_Response
base_url = 'http://localhost:8983' base_url = 'http://localhost:8983'
@ -72,27 +73,21 @@ def request(query, params):
return params return params
def response(resp): def response(resp: SXNG_Response) -> EngineResults:
resp_json = __get_response(resp)
results = []
for result in resp_json['response']['docs']:
r = {key: str(value) for key, value in result.items()}
if len(r) == 0:
continue
r['template'] = 'key-value.html'
results.append(r)
return results
def __get_response(resp):
try: try:
resp_json = loads(resp.text) resp_json = resp.json()
except Exception as e: except Exception as e:
raise SearxEngineAPIException("failed to parse response") from e raise SearxEngineAPIException("failed to parse response") from e
if 'error' in resp_json: if "error" in resp_json:
raise SearxEngineAPIException(resp_json['error']['msg']) raise SearxEngineAPIException(resp_json["error"]["msg"])
return resp_json res = EngineResults()
for result in resp_json["response"]["docs"]:
kvmap = {key: str(value) for key, value in result.items()}
if not kvmap:
continue
res.add(res.types.KeyValue(kvmap=kvmap))
return res

View File

@ -2,6 +2,14 @@
"""SQLite is a small, fast and reliable SQL database engine. It does not require """SQLite is a small, fast and reliable SQL database engine. It does not require
any extra dependency. any extra dependency.
Configuration
=============
The engine has the following (additional) settings:
- :py:obj:`result_type`
Example Example
======= =======
@ -18,29 +26,32 @@ Query to test: ``!mediathekview concert``
.. code:: yaml .. code:: yaml
- name: mediathekview - name: mediathekview
engine: sqlite engine: sqlite
disabled: False shortcut: mediathekview
categories: general categories: [general, videos]
result_template: default.html result_type: MainResult
database: searx/data/filmliste-v2.db database: searx/data/filmliste-v2.db
query_str: >- query_str: >-
SELECT title || ' (' || time(duration, 'unixepoch') || ')' AS title, SELECT title || ' (' || time(duration, 'unixepoch') || ')' AS title,
COALESCE( NULLIF(url_video_hd,''), NULLIF(url_video_sd,''), url_video) AS url, COALESCE( NULLIF(url_video_hd,''), NULLIF(url_video_sd,''), url_video) AS url,
description AS content description AS content
FROM film FROM film
WHERE title LIKE :wildcard OR description LIKE :wildcard WHERE title LIKE :wildcard OR description LIKE :wildcard
ORDER BY duration DESC ORDER BY duration DESC
Implementations Implementations
=============== ===============
""" """
import typing
import sqlite3 import sqlite3
import contextlib import contextlib
engine_type = 'offline' from searx.result_types import EngineResults
from searx.result_types import MainResult, KeyValue
engine_type = "offline"
database = "" database = ""
"""Filename of the SQLite DB.""" """Filename of the SQLite DB."""
@ -48,9 +59,11 @@ database = ""
query_str = "" query_str = ""
"""SQL query that returns the result items.""" """SQL query that returns the result items."""
result_type: typing.Literal["MainResult", "KeyValue"] = "KeyValue"
"""The result type can be :py:obj:`MainResult` or :py:obj:`KeyValue`."""
limit = 10 limit = 10
paging = True paging = True
result_template = 'key-value.html'
def init(engine_settings): def init(engine_settings):
@ -80,9 +93,8 @@ def sqlite_cursor():
yield cursor yield cursor
def search(query, params): def search(query, params) -> EngineResults:
results = [] res = EngineResults()
query_params = { query_params = {
'query': query, 'query': query,
'wildcard': r'%' + query.replace(' ', r'%') + r'%', 'wildcard': r'%' + query.replace(' ', r'%') + r'%',
@ -97,9 +109,11 @@ def search(query, params):
col_names = [cn[0] for cn in cur.description] col_names = [cn[0] for cn in cur.description]
for row in cur.fetchall(): for row in cur.fetchall():
item = dict(zip(col_names, map(str, row))) kvmap = dict(zip(col_names, map(str, row)))
item['template'] = result_template if result_type == "MainResult":
logger.debug("append result --> %s", item) item = MainResult(**kvmap) # type: ignore
results.append(item) else:
item = KeyValue(kvmap=kvmap)
res.add(item)
return results return res

View File

@ -1888,15 +1888,15 @@ engines:
# For this demo of the sqlite engine download: # For this demo of the sqlite engine download:
# https://liste.mediathekview.de/filmliste-v2.db.bz2 # https://liste.mediathekview.de/filmliste-v2.db.bz2
# and unpack into searx/data/filmliste-v2.db # and unpack into searx/data/filmliste-v2.db
# Query to test: "!demo concert" # Query to test: "!mediathekview concert"
# #
# - name: demo # - name: mediathekview
# engine: sqlite # engine: sqlite
# shortcut: demo # shortcut: mediathekview
# categories: general # categories: [general, videos]
# result_template: default.html # result_type: MainResult
# database: searx/data/filmliste-v2.db # database: searx/data/filmliste-v2.db
# query_str: >- # query_str: >-
# SELECT title || ' (' || time(duration, 'unixepoch') || ')' AS title, # SELECT title || ' (' || time(duration, 'unixepoch') || ')' AS title,
# COALESCE( NULLIF(url_video_hd,''), NULLIF(url_video_sd,''), url_video) AS url, # COALESCE( NULLIF(url_video_hd,''), NULLIF(url_video_sd,''), url_video) AS url,
# description AS content # description AS content

View File

@ -1,11 +0,0 @@
<table>
{% for key, value in result.items() %}
{% if key in ['engine', 'engines', 'template', 'score', 'category', 'positions', 'parsed_url'] %}
{% continue %}
{% endif %}
<tr>
<td><b>{{ key|upper }}</b>: {{ value }}</td>
</tr>
{% endfor %}
</table>
<div class="engines">{% for engine in result.engines %}<span>{{ engine }}</span>{% endfor %}</div>{{- '' -}}