on master help sign in
Add recoll engine (#2325)

recoll is a local search engine based on Xapian: http://www.lesbonscomptes.com/recoll/ By itself recoll does not offer web or API access, this can be achieved using recoll-webui: https://framagit.org/medoc92/recollwebui.git This engine uses a custom 'files' result template set `base_url` to the location where recoll-webui can be reached set `dl_prefix` to a location where the file hierarchy as indexed by recoll can be reached set `search_dir` to the part of the indexed file hierarchy to be searched, use an empty string to search the entire search domain

commit4a36a3044d6e39bc60d026d99ed7a010f6505a5f authorNoémi Ványi <kvch@users.noreply.github.com> dateMon 30 Nov 2020, 08:35 (6 years ago) parent93c2603
8 files changed+298−6
docs/admin/engines.rst+19−6 docs/admin/engines/recoll.rst+50 docs/dev/engine_overview.rst+3 searx/engines/recoll.py+104 searx/settings.yml+22 searx/static/themes/oscar/less/logicodev/results.less+19 searx/templates/oscar/macros.html+26 searx/templates/oscar/result_templates/files.html+55
docs/admin/engines.rst+19−6
@@ -1,14 +1,28 @@
1−.. _engines generic:
2−
31=======
42Engines
53=======
64
5+Special Engine Settings
6+=======================
7+
78.. sidebar:: Further reading ..
89
910 - :ref:`settings engine`
10− - :ref:`engine settings`
11− - :ref:`engine file`
11+ - :ref:`engine settings` & :ref:`engine file`
12+
13+.. toctree::
14+ :maxdepth: 1
15+
16+ engines/recoll.rst
17+
18+
19+.. _engines generic:
20+
21+General Engine Settings
22+=======================
23+
24+Explanation of the :ref:`general engine configuration` shown in the table
25+:ref:`configured engines`.
1226
1327============= =========== ==================== ============
1428:ref:`engine settings` :ref:`engine file`
@@ -30,8 +44,6 @@ Disabled **D**
3044Show errors **DE**
3145============= =========== =================================
3246
33−Configuration defaults (at built time):
34−
3547.. _configured engines:
3648
3749.. jinja:: webapp
@@ -73,3 +85,4 @@ Configuration defaults (at built time):
7385 - {{(mod.display_error_messages and "y") or ""}}
7486
7587 {% endfor %}
88+
docs/admin/engines/recoll.rstnew file+50
@@ -0,0 +1,50 @@
1+.. _engine recoll:
2+
3+======
4+Recoll
5+======
6+
7+.. sidebar:: info
8+
9+ - `Recoll <https://www.lesbonscomptes.com/recoll/>`_
10+ - `recoll-webui <https://framagit.org/medoc92/recollwebui.git>`_
11+
12+Recoll_ is a desktop full-text search tool based on Xapian. By itself Recoll_
13+does not offer web or API access, this can be achieved using recoll-webui_
14+
15+
16+
17+Configuration
18+=============
19+
20+You must configure the following settings:
21+
22+``base_url``:
23+ Location where recoll-webui can be reached.
24+
25+``mount_prefix``:
26+ Location where the file hierarchy is mounted on your *local* filesystem.
27+
28+``dl_prefix``:
29+ Location where the file hierarchy as indexed by recoll can be reached.
30+
31+``search_dir``:
32+ Part of the indexed file hierarchy to be search, if empty the full domain is
33+ searched.
34+
35+
36+Example
37+=======
38+
39+Scenario:
40+
41+#. Recoll indexes a local filesystem mounted in ``/export/documents/reference``,
42+#. the Recoll search inteface can be reached at https://recoll.example.org/ and
43+#. the contents of this filesystem can be reached though https://download.example.org/reference
44+
45+.. code:: yaml
46+
47+ base_url: https://recoll.example.org/
48+ mount_prefix: /export/documents
49+ dl_prefix: https://download.example.org
50+ search_dir: ''
docs/dev/engine_overview.rst+3
@@ -18,6 +18,9 @@ engines. Adapters are stored under the folder :origin:`searx/engines`.
1818 :depth: 3
1919 :backlinks: entry
2020
21+
22+.. _general engine configuration:
23+
2124general engine configuration
2225============================
2326
searx/engines/recoll.pynew file+104
@@ -0,0 +1,104 @@
1+"""
2+ Recoll (local search engine)
3+
4+ @using-api yes
5+ @results JSON
6+ @stable yes
7+ @parse url, content, size, abstract, author, mtype, subtype, time, \
8+ filename, label, type, embedded
9+"""
10+
11+from datetime import date, timedelta
12+from json import loads
13+from urllib.parse import urlencode, quote
14+
15+# engine dependent config
16+time_range_support = True
17+
18+# parameters from settings.yml
19+base_url = None
20+search_dir = ''
21+mount_prefix = None
22+dl_prefix = None
23+
24+# embedded
25+embedded_url = '<{ttype} controls height="166px" ' +\
26+ 'src="{url}" type="{mtype}"></{ttype}>'
27+
28+
29+# helper functions
30+def get_time_range(time_range):
31+ sw = {
32+ 'day': 1,
33+ 'week': 7,
34+ 'month': 30,
35+ 'year': 365
36+ }
37+
38+ offset = sw.get(time_range, 0)
39+ if not offset:
40+ return ''
41+
42+ return (date.today() - timedelta(days=offset)).isoformat()
43+
44+
45+# do search-request
46+def request(query, params):
47+ search_after = get_time_range(params['time_range'])
48+ search_url = base_url + 'json?{query}&highlight=0'
49+ params['url'] = search_url.format(query=urlencode({
50+ 'query': query,
51+ 'after': search_after,
52+ 'dir': search_dir}))
53+
54+ return params
55+
56+
57+# get response from search-request
58+def response(resp):
59+ results = []
60+
61+ response_json = loads(resp.text)
62+
63+ if not response_json:
64+ return []
65+
66+ for result in response_json.get('results', []):
67+ title = result['label']
68+ url = result['url'].replace('file://' + mount_prefix, dl_prefix)
69+ content = '{}'.format(result['snippet'])
70+
71+ # append result
72+ item = {'url': url,
73+ 'title': title,
74+ 'content': content,
75+ 'template': 'files.html'}
76+
77+ if result['size']:
78+ item['size'] = int(result['size'])
79+
80+ for parameter in ['filename', 'abstract', 'author', 'mtype', 'time']:
81+ if result[parameter]:
82+ item[parameter] = result[parameter]
83+
84+ # facilitate preview support for known mime types
85+ if 'mtype' in result and '/' in result['mtype']:
86+ (mtype, subtype) = result['mtype'].split('/')
87+ item['mtype'] = mtype
88+ item['subtype'] = subtype
89+
90+ if mtype in ['audio', 'video']:
91+ item['embedded'] = embedded_url.format(
92+ ttype=mtype,
93+ url=quote(url.encode('utf8'), '/:'),
94+ mtype=result['mtype'])
95+
96+ if mtype in ['image'] and subtype in ['bmp', 'gif', 'jpeg', 'png']:
97+ item['img_src'] = url
98+
99+ results.append(item)
100+
101+ if 'nres' in response_json:
102+ results.append({'number_of_results': response_json['nres']})
103+
104+ return results
searx/settings.yml+22
@@ -650,6 +650,28 @@ engines:
650650 shortcut : qws
651651 categories : social media
652652
653+# - name: library
654+# engine: recoll
655+# shortcut: lib
656+# base_url: 'https://recoll.example.org/'
657+# search_dir: ''
658+# mount_prefix: /export
659+# dl_prefix: 'https://download.example.org'
660+# timeout: 30.0
661+# categories: files
662+# disabled: True
663+
664+# - name: recoll library reference
665+# engine: recoll
666+# base_url: 'https://recoll.example.org/'
667+# search_dir: reference
668+# mount_prefix: /export
669+# dl_prefix: 'https://download.example.org'
670+# shortcut: libr
671+# timeout: 30.0
672+# categories: files
673+# disabled: True
674+
653675 - name : reddit
654676 engine : reddit
655677 shortcut : re
searx/static/themes/oscar/less/logicodev/results.less+19
@@ -51,6 +51,11 @@
5151 float: right;
5252}
5353
54+.result-abstract {
55+ margin-top: 0.5em;
56+ margin-bottom: 0.8em;
57+}
58+
5459.external-link {
5560 color: @dark-green;
5661 font-size: 12px;
@@ -124,6 +129,20 @@
124129 }
125130}
126131
132+.result-metadata {
133+ clear: both;
134+ margin: 1em;
135+
136+ td {
137+ padding-right: 1em;
138+ color: @gray;
139+ }
140+
141+ td:first-of-type {
142+ color: @dark-gray;
143+ }
144+}
145+
127146// map formating of results
128147.result-map {
129148 clear: both;
searx/templates/oscar/macros.html+26
@@ -47,6 +47,20 @@
4747 {%- endif -%}
4848{%- endmacro %}
4949
50+<!-- Draw result footer without cache link -->
51+{% macro result_footer_nocache(result) -%}
52+ <div class="clearfix"></div>
53+ <div class="pull-right">
54+ {% for engine in result.engines %}
55+ <span class="label label-default">{{ engine }}</span>
56+ {% endfor %}
57+ {% if proxify %}
58+ <small>{{ result_link(proxify(result.url), icon('sort') + _('proxied'), "text-info") }}</small>
59+ {% endif %}
60+</div>
61+<div class="external-link">{{ result.pretty_url }}</div>
62+{%- endmacro %}
63+
5064<!-- Draw result footer -->
5165{% macro result_footer_rtl(result, id) -%}
5266 <div class="clearfix"></div>{{- "" -}}
@@ -68,6 +82,18 @@
6882 {%- endif %}
6983{%- endmacro %}
7084
85+<!-- Draw result footer without cache link -->
86+{% macro result_footer_nocache_rtl(result) -%}
87+ <div class="clearfix"></div>
88+ {% for engine in result.engines %}
89+ <span class="label label-default">{{ engine }}</span>
90+ {% endfor %}
91+ {% if proxify %}
92+ <small>{{ result_link(proxify(result.url), icon('sort') + _('proxied'), "text-info") }}</small>
93+ {% endif %}
94+ <div class="external-link">{{ result.pretty_url }}</div>
95+{%- endmacro %}
96+
7197{% macro preferences_item_header(info, label, rtl, id) -%}
7298 {% if rtl %}
7399 <div class="row form-group">
searx/templates/oscar/result_templates/files.htmlnew file+55
@@ -0,0 +1,55 @@
1+{% from 'oscar/macros.html' import result_header, result_sub_header, result_footer_nocache, result_footer_nocache_rtl, icon with context %}
2+
3+{{ result_header(result, favicons) }}
4+{{ result_sub_header(result) }}
5+
6+{% if result.embedded %}
7+ <small> &bull; <a class="text-info btn-collapse collapsed cursor-pointer media-loader disabled_if_nojs" data-toggle="collapse" data-target="#result-media-{{ index }}" data-btn-text-collapsed="{{ _('show media') }}" data-btn-text-not-collapsed="{{ _('hide media') }}">
8+ {% if result.mtype == 'audio' %}{{ icon('music') }}
9+ {% elif result.mtype == 'video' %} {{ icon('film') }}
10+ {% endif %} {{ _('show media') }}</a></small>
11+{% endif %}
12+
13+{% if result.embedded %}
14+<div id="result-media-{{ index }}" class="collapse">
15+ {{ result.embedded|safe }}
16+</div>
17+{% endif %}
18+
19+{% if result.abstract %}<p class="result-content result-abstract">{{ result.abstract|safe }}</p>{% endif %}
20+
21+{% if result.img_src %}
22+<div class="container-fluid">
23+ <div class="row">
24+<img src="{{ image_proxify(result.img_src) }}" alt="{{ result.title|striptags }}" title="{{ result.title|striptags }}" style="width: auto; max-height: 60px; min-height: 60px;" class="col-xs-2 col-sm-4 col-md-4 result-content">
25+{% if result.content %}<p class="result-content col-xs-8 col-sm-8 col-md-8">{{ result.content|safe }}</p>{% endif %}
26+ </div>
27+</div>
28+{% else %}
29+{% if result.content %}<p class="result-content">{{ result.content|safe }}</p>{% endif %}
30+{% endif %}
31+
32+<table class="result-metadata result-content">
33+{% if result.author %}<tr><td>{{ _('Author') }}</td><td>{{ result.author|safe }}</td></tr>{% endif %}
34+
35+{% if result.filename %}<tr><td>{{ _('Filename') }}</td><td>{{ result.filename|safe }}</td></tr>{% endif %}
36+
37+{% if result.size %}<tr><td>{{ _('Filesize') }}</td><td>
38+ {% if result.size < 1024 %}{{ result.size }} {{ _('Bytes') }}
39+ {% elif result.size < 1024*1024 %}{{ '{0:0.2f}'.format(result.size/1024) }} {{ _('kiB') }}
40+ {% elif result.size < 1024*1024*1024 %}{{ '{0:0.2f}'.format(result.size/1024/1024) }} {{ _('MiB') }}
41+ {% elif result.size < 1024*1024*1024*1024 %}{{ '{0:0.2f}'.format(result.size/1024/1024/1024) }} {{ _('GiB') }}
42+ {% else %}{{ '{0:0.2f}'.format(result.size/1024/1024/1024/1024) }} {{ _('TiB') }}{% endif %}
43+ </td></tr>
44+{% endif %}
45+
46+{% if result.time %}<tr><td>{{ _('Date') }}</td><td>{{ result.time|safe }}</td></tr>{% endif %}
47+
48+{% if result.mtype %}<tr><td>{{ _('Type') }}</td><td>{{ result.mtype|safe }}/{{ result.subtype|safe }}</td></tr>{% endif %}
49+</table>
50+
51+{% if rtl %}
52+{{ result_footer_nocache_rtl(result) }}
53+{% else %}
54+{{ result_footer_nocache(result) }}
55+{% endif %}