Skip to content

Commit 721da5b

Browse files
committed
OARec: facets and distributed search updates
1 parent 5e7b2ff commit 721da5b

11 files changed

Lines changed: 277 additions & 21 deletions

File tree

pycsw/ogc/api/oapi.py

Lines changed: 109 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ def gen_oapi(config, oapi_filepath, mode='ogcapi-records'):
101101
'description': 'a reference to the formal definition of the type', # noqa
102102
'format': 'url',
103103
'type': 'string'
104+
},
105+
'facet': {
106+
'description': 'whether the queryable is also a facet',
107+
'type': 'boolean'
104108
}
105109
},
106110
'required': [
@@ -109,6 +113,75 @@ def gen_oapi(config, oapi_filepath, mode='ogcapi-records'):
109113
],
110114
'type': 'object'
111115
}
116+
oapi['components']['schemas']['queryables'] = {
117+
'properties': {
118+
'id': {
119+
'type': 'string',
120+
'description': 'collection identifier'
121+
},
122+
'title': {
123+
'type': 'string',
124+
'description': 'collection title'
125+
},
126+
'queryables2': {
127+
'items': {
128+
'$ref': '#/components/schemas/queryable'
129+
},
130+
'type': 'array'
131+
}
132+
},
133+
'required': [
134+
'queryables'
135+
],
136+
'type': 'object'
137+
}
138+
139+
oapi['components']['responses']['Facets'] = {
140+
'content': {
141+
'application/facets+json': {
142+
'schema': {
143+
'$ref': '#/components/schemas/facets'
144+
}
145+
}
146+
},
147+
'description': 'successful facets operation'
148+
}
149+
oapi['components']['schemas']['facets'] = {
150+
'type': 'object',
151+
'properties': {
152+
'id': {
153+
'type': 'string',
154+
'description': 'collection identifier'
155+
},
156+
'title': {
157+
'type': 'string',
158+
'description': 'collection title'
159+
},
160+
'facets': {
161+
'type': 'object',
162+
'patternProperties': {
163+
'^.*': {
164+
'type': 'object',
165+
'properties': {
166+
'type': {
167+
'type': 'string',
168+
'enum': [
169+
'term',
170+
'histogram',
171+
'filter'
172+
]
173+
},
174+
'property': {
175+
'type': 'string',
176+
'description': 'property name'
177+
}
178+
}
179+
}
180+
}
181+
}
182+
}
183+
}
184+
112185
oapi['components']['schemas']['queryables'] = {
113186
'properties': {
114187
'queryables': {
@@ -388,6 +461,30 @@ def gen_oapi(config, oapi_filepath, mode='ogcapi-records'):
388461
}
389462

390463
oapi['paths']['/collections/{collectionId}/queryables'] = path2
464+
465+
path = {
466+
'get': {
467+
'tags': ['Facets'],
468+
'summary': 'Facets page',
469+
'description': 'Facets page',
470+
'operationId': 'getFacets',
471+
'parameters': [
472+
{'$ref': '#/components/parameters/f'},
473+
{'$ref': '#/components/parameters/collectionId'}
474+
],
475+
'responses': {
476+
'200': {
477+
'$ref': '#/components/responses/Facets'
478+
},
479+
'500': {
480+
'$ref': '#/components/responses/ServerError'
481+
}
482+
}
483+
}
484+
}
485+
486+
oapi['paths']['/collections/{collectionId}/facets'] = path
487+
391488
oapi['components']['parameters']['collectionId']['default'] = 'metadata:main' # noqa
392489

393490
path = {
@@ -419,18 +516,19 @@ def gen_oapi(config, oapi_filepath, mode='ogcapi-records'):
419516
'summary': 'Federated catalogs page',
420517
'description': 'Federated catalogs page',
421518
'operationId': 'getFederatedCatalog',
422-
'parameters': [
423-
{'$ref': '#/components/parameters/collectionId'},
424-
{'name': 'catalogId',
425-
'in': 'path',
426-
'description': 'catalog ID',
427-
'required': True,
428-
'schema': {
519+
'parameters': [{
520+
'$ref': '#/components/parameters/collectionId'
521+
}, {
522+
'name': 'catalogId',
523+
'in': 'path',
524+
'description': 'catalog ID',
525+
'required': True,
526+
'schema': {
429527
'type': 'string'
430528
}
431-
},
432-
{'$ref': '#/components/parameters/f'}
433-
],
529+
}, {
530+
'$ref': '#/components/parameters/f'
531+
}],
434532
'responses': {
435533
'200': {
436534
'$ref': '#/components/responses/FederatedCatalog'
@@ -442,7 +540,7 @@ def gen_oapi(config, oapi_filepath, mode='ogcapi-records'):
442540
}
443541
}
444542

445-
oapi['paths']['/collections/{collectionId}/federatedCatalogs/{catalogId}'] = path
543+
oapi['paths']['/collections/{collectionId}/federatedCatalogs/{catalogId}'] = path # noqa
446544

447545
path = {
448546
'get': {

pycsw/ogc/api/records.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@
7676
'http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/sorting',
7777
'http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/json',
7878
'http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/html',
79+
'http://www.opengis.net/spec/ogcapi-records-2/1.0/conf/simple',
80+
'http://www.opengis.net/spec/ogcapi-records-4/1.0/conf/federated-search',
7981
'http://www.opengis.net/spec/cql2/1.0/conf/cql2-json',
8082
'http://www.opengis.net/spec/cql2/1.0/conf/cql2-text'
8183
]
@@ -529,6 +531,8 @@ def queryables(self, headers_, args, collection='metadata:main'):
529531
for key, value in properties.items():
530532
if key in self.repository.query_mappings or key == 'geometry':
531533
properties2[key] = value
534+
if key in self.facets:
535+
properties2[key]['facet'] = True
532536

533537
if collection == 'metadata:main':
534538
title = self.config['metadata']['identification']['title']
@@ -548,6 +552,53 @@ def queryables(self, headers_, args, collection='metadata:main'):
548552

549553
return self.get_response(200, headers_, response, 'queryables.html')
550554

555+
def facets_(self, headers_, args, collection='metadata:main'):
556+
"""
557+
Provide collection facets
558+
559+
:param headers_: copy of HEADERS object
560+
:param args: request parameters
561+
:param collection: name of collection
562+
563+
:returns: tuple of headers, status code, content
564+
"""
565+
566+
facets_ = {}
567+
headers_['Content-Type'] = self.get_content_type(headers_, args)
568+
569+
if 'json' in headers_['Content-Type']:
570+
headers_['Content-Type'] = 'application/facets+json'
571+
572+
if collection not in self.get_collections(collection=collection):
573+
msg = 'Invalid collection'
574+
LOGGER.exception(msg)
575+
return self.get_exception(400, headers_, 'InvalidParameterValue', msg)
576+
577+
for value in self.config['repository']['facets']:
578+
facets_[value] = {
579+
'type': 'term',
580+
'property': value,
581+
'sortedBy': 'count'
582+
}
583+
584+
if collection == 'metadata:main':
585+
title = self.config['metadata']['identification']['title']
586+
else:
587+
title = self.config['metadata']['identification']['title']
588+
virtual_collection = self.repository.query_ids([collection])[0]
589+
title = virtual_collection.title
590+
591+
response = {
592+
'id': collection,
593+
'type': 'object',
594+
'title': title,
595+
'facets': facets_,
596+
'$schema': 'http://json-schema.org/draft/2019-09/schema',
597+
'$id': f"{self.config['server']['url']}/collections/{collection}/facets"
598+
}
599+
600+
return self.get_response(200, headers_, response, 'facets.html')
601+
551602
def items(self, headers_, json_post_data, args, collection='metadata:main'):
552603
"""
553604
Provide collection items
@@ -818,6 +869,7 @@ def items(self, headers_, json_post_data, args, collection='metadata:main'):
818869
distributed = str2bool(args.get('distributedsearch', False))
819870

820871
if distributed:
872+
args.pop('distributedsearch')
821873
distributed_search = self.config.get('distributedsearch', {})
822874
merge_results = distributed_search.get('merge_results', False)
823875

@@ -833,7 +885,6 @@ def items(self, headers_, json_post_data, args, collection='metadata:main'):
833885
}
834886
try:
835887
w = Records(fc_url)
836-
args.pop('distributedsearch')
837888
fc_results = w.collection_items(fc_collection, **args)
838889
for feature in fc_results['features']:
839890
if merge_results:
@@ -849,6 +900,8 @@ def items(self, headers_, json_post_data, args, collection='metadata:main'):
849900
for key, value in fsrs.items():
850901
response['features'].extend(value['features'])
851902

903+
args['distributedSearch'] = 'true'
904+
852905
LOGGER.debug('Creating links')
853906

854907
link_args = {**args}

pycsw/templates/collection.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ <h2>{{ data['title'] }}</h2>
3535
<td>
3636
<a title="items" href="{{ config['server']['url'] }}/collections/{{ data.id }}/items">items</a>
3737
<a title="queryables" href="{{ config['server']['url'] }}/collections/{{ data.id }}/queryables">queryables</a>
38+
<a title="facets" href="{{ config['server']['url'] }}/collections/{{ data.id }}/facets">facets</a>
3839
{% if data.id == 'metadata:main' and config['distributedsearch'] %}
3940
<a title="federated catalogs" href="{{ config['server']['url'] }}/collections/{{ data.id }}/federatedCatalogs">federated catalogs</a>
4041
{% endif %}

pycsw/templates/collections.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ <h2>Collections</h2>
3434
<td>
3535
<a title="items" href="{{ config['server']['url'] }}/collections/{{ col.id }}/items">items</a>
3636
<a title="queryables" href="{{ config['server']['url'] }}/collections/{{ col.id }}/queryables">queryables</a>
37+
<a title="facets" href="{{ config['server']['url'] }}/collections/{{ col.id }}/facets">facets</a>
3738
{% if col.id == 'metadata:main' and config['distributedsearch'] %}
3839
<a title="federated catalogs" href="{{ config['server']['url'] }}/collections/{{ col.id }}/federatedCatalogs">federated catalogs</a>
3940
{% endif %}

pycsw/templates/facets.html

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{% extends "_base.html" %}
2+
{% block title %}{{ super() }} Facets {% endblock %}
3+
4+
{% block crumbs %}
5+
{{ super() }} /
6+
<a href="{{ config['server']['url'] }}/collections">Collections</a> /
7+
<a href="{{ config['server']['url'] }}/collections/{{ data['id'] }}">{{ data['title'] }}</a> /
8+
<a href="{{ config['server']['url'] }}/collections/{{ data['id'] }}/facets">Facets</a>
9+
{% endblock %}
10+
11+
{% block body %}
12+
13+
<section id="queryables">
14+
<h2>Facets</h2>
15+
<h2>{{ data['title'] }}</h2>
16+
17+
<table class="table table-striped table-hover">
18+
<thead>
19+
<tr>
20+
<th>Name</th>
21+
<th>Type</th>
22+
<th>Facet</th>
23+
</tr>
24+
</thead>
25+
<tbody>
26+
{% for qname, qinfo in data['facets'].items() %}
27+
{% if qname == 'geometry' %}
28+
<tr>
29+
<td colspan="3"><a href="{{ qinfo['$ref'] }}">{{ qname }}</a></td>
30+
</tr>
31+
{% else %}
32+
<tr>
33+
<td>{{ qname }}</td>
34+
<td>
35+
<code>{{ qinfo['type'] }}</code>
36+
{% if 'enum' in qinfo %}
37+
<ul>
38+
{% for value in qinfo['enum'] %}
39+
<li><i>{{ value }}</i></li>
40+
{% endfor %}
41+
</ul>
42+
{% endif %}
43+
</td>
44+
{% if qname in config['repository'].get('facets', []) %}
45+
<td>true</td>
46+
{% else %}
47+
<td></td>
48+
{% endif %}
49+
</tr>
50+
{% endif %}
51+
{% endfor %}
52+
</tbody>
53+
</table>
54+
</section>
55+
56+
{% endblock %}

pycsw/templates/queryables.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ <h2>{{ data['title'] }}</h2>
1919
<tr>
2020
<th>Name</th>
2121
<th>Type</th>
22+
<th>Facet</th>
2223
</tr>
2324
</thead>
2425
<tbody>
2526
{% for qname, qinfo in data['properties'].items() %}
2627
{% if qname == 'geometry' %}
2728
<tr>
28-
<td colspan="2"><a href="{{ qinfo['$ref'] }}">{{ qname }}</a></td>
29+
<td colspan="3"><a href="{{ qinfo['$ref'] }}">{{ qname }}</a></td>
2930
</tr>
3031
{% else %}
3132
<tr>
@@ -40,6 +41,11 @@ <h2>{{ data['title'] }}</h2>
4041
</ul>
4142
{% endif %}
4243
</td>
44+
{% if qname in config['repository'].get('facets', []) %}
45+
<td>true</td>
46+
{% else %}
47+
<td></td>
48+
{% endif %}
4349
</tr>
4450
{% endif %}
4551
{% endfor %}

pycsw/wsgi_flask.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,20 @@ def queryables(collection='metadata:main'):
206206
collection))
207207

208208

209+
@BLUEPRINT.route('/collections/<collection>/facets')
210+
def facets(collection='metadata:main'):
211+
"""
212+
OGC API collection facets endpoint
213+
214+
:param collection: collection name
215+
216+
:returns: HTTP response
217+
"""
218+
219+
return get_response(api_.facets_(dict(request.headers), request.args,
220+
collection))
221+
222+
209223
@BLUEPRINT.route('/collections/<collection>/federatedCatalogs')
210224
def federated_catalogues(collection='metadata:main'):
211225
"""

tests/functionaltests/suites/oarec/conftest.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Tom Kralidis <tomkralidis@gmail.com>
55
#
66
# Copyright (c) 2023 Ricardo Garcia Silva
7-
# Copyright (c) 2024 Tom Kralidis
7+
# Copyright (c) 2026 Tom Kralidis
88
#
99
# Permission is hereby granted, free of charge, to any person
1010
# obtaining a copy of this software and associated documentation
@@ -111,7 +111,10 @@ def config():
111111
},
112112
'repository': {
113113
'database': 'sqlite:///tests/functionaltests/suites/cite/data/cite.db', # noqa
114-
'table': 'records'
114+
'table': 'records',
115+
'facets': [
116+
'type'
117+
]
115118
}
116119
}
117120

0 commit comments

Comments
 (0)