Usage

UI for Invenio-Search.

Invenio-Search-UI is responsible for providing an interactive user interface for displaying, filtering and navigating search results from the various endpoints defined in Invenio-Records-REST. This is achieved through the usage of the Invenio-Search-JS AngularJS package and its configuration inside Jinja and AngularJS templates.

Although a default /search endpoint is provided, meant for displaying search results for the main type of records an Invenio instance is storing, it is also possible to define multiple search result pages by extending and configuring some base Jinja and AngularJS templates.

Initialization

Note

The following commands can be either run in a Python shell or written to a separate app.py file which can then be run via python app.py or by running export FLASK_APP=app.py and using the flask CLI tools. You can take inspiration from the Example Application on how the end result of this guide will look like.

To make sure that you have all of the dependencies used installed you should also run pip install invenio-search-ui[all] first.

First create a Flask application:

>>> from flask import Flask
>>> app = Flask('myapp')

There are several dependencies that should be initialized in order to make Invenio-Search-UI work correctly.

>>> from invenio_db import InvenioDB
>>> from invenio_pidstore import InvenioPIDStore
>>> from invenio_records import InvenioRecords
>>> from invenio_rest import InvenioREST
>>> from invenio_search import InvenioSearch
>>> from invenio_indexer import InvenioIndexer
>>> from invenio_theme import InvenioTheme
>>> from invenio_i18n import InvenioI18N
>>> app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
>>> ext_db = InvenioDB(app)
>>> ext_pid = InvenioPIDStore(app)
>>> ext_records = InvenioRecords(app)
>>> ext_rest = InvenioREST(app)
>>> ext_theme = InvenioTheme(app)
>>> ext_i18n = InvenioI18N(app)
>>> ext_indexer = InvenioIndexer(app)
>>> ext_search = InvenioSearch(app)

Register the JavaScript bundle, containing Invenio-Search-JS:

>>> from invenio_assets import InvenioAssets
>>> from invenio_search_ui.bundles import js
>>> ext_assets = InvenioAssets(app)
>>> ext_assets.env.register('invenio_search_ui_search_js', js)
<NpmBundle ...>

Before we initialize the Invenio-Search-UI extension, we need to have some REST API endpoints configured to expose our records. For more detailed documentation on configuring the records REST API, you can look into invenio-records-rest.

By default Records REST exposes a /api/records/ endpoint, which resolves integer record identifiers to internal record objects. It uses though a custom Flask URL converter to resolve this integer to a Persistent Identifier, which needs to be registered:

>>> from invenio_records_rest import InvenioRecordsREST
>>> from invenio_records_rest.utils import PIDConverter
>>> app.url_map.converters['pid'] = PIDConverter
>>> ext_records_rest = InvenioRecordsREST(app)

Now we can initialize Invenio-Search-UI and register its blueprint:

>>> from invenio_search_ui import InvenioSearchUI
>>> from invenio_search_ui.views import blueprint
>>> ext_search_ui = InvenioSearchUI(app)
>>> app.register_blueprint(blueprint)

In order for the following examples to work, you need to work within an Flask application context so let’s push one:

>>> ctx = app.app_context()
>>> ctx.push()

Also, for the examples to work we need to create the database and tables (note, in this example we use an in-memory SQLite database):

>>> from invenio_db import db
>>> db.create_all()

Building Assets

In order to render the search results page, you will have to build the JavaScript and CSS assets that the page depends on. To do so you will have to run the following commands:

$ export FLASK_APP=app.py
$ flask collect
$ flask npm
$ cd static && npm install && cd ..
$ flask assets build

Record data

Last, but not least, we have to create and index a record:

>>> from uuid import uuid4
>>> from invenio_records.api import Record
>>> from invenio_pidstore.providers.recordid import RecordIdProvider
>>> from invenio_indexer.api import RecordIndexer
>>> rec = Record.create({
...     'title': 'My title',
...     'description': 'My record decription',
...     'type': 'article',
...     'creators': [{'name': 'Doe, John'}, {'name': 'Roe, Jane'}],
...     'status': 'published',
... }, id_=uuid4())
>>> provider = RecordIdProvider.create(object_type='rec', object_uuid=rec.id)
>>> db.session.commit()
>>> RecordIndexer().index_by_id(str(rec.id))
{...}

Feel free to create more records in a similar fashion.

Customizing templates

Search components

The building blocks for the search result page are all the <invenio-search-...> Angular directives defined in InvenioSearchJS. You can think of them as the UI components of a classic search page. All of the available directives are listed below (with their default template files):

  • <invenio-search-results> - The actual result item display (default.html)
  • <invenio-search-bar> - Search input box for queries (searchbar.html)
  • <invenio-search-pagination> - Pagination controls for navigating the search result pages (pagination.html)
  • <invenio-search-sort-order> - Sort order of results, i.e. ascending/descending (togglebutton.html)
  • <invenio-search-facets> - Faceting options for the search results (facets.html)
  • <invenio-search-loading> - Loading indicator for the REST API request (loading.html)
  • <invenio-search-count> - The number of search results (count.html)
  • <invenio-search-error> - Errors returned by the REST API, e.g. 4xx or 5xx (error.html)
  • <invenio-search-range> - Date or numeric range filtering (range.html)
  • <invenio-search-select-box> - Select box for further filtering (selectbox.html)

Each one of them accepts attributes for configuring their specific behavior. All of them though accept a template attribute specifying an Angular HTML template file which will be used for their rendering, thus defining the component’s visual aspects.

In order for them to function, they need to be placed inside <invenio-search> tags, which also contain the configuration for the general search mechanics, like the REST API endpoint for the search results, HTTP headers for the request, extra querystring parameters, etc. You can read more about these directives and their parameters in the documentation of Invenio-Search-JS.

These components are placed and configured inside Jinja templates, where one has the choice to either override individual pre-existing Jinja blocks or even completely rearrange the way the componentns are organize in the template.

Note

You can find a full example of this type of configuration and templates in search.html and static/templates.

Creating a new search page

Let’s create a new search page exclusively for records. For that we’ll need to add a new route to our application that will render our custom search page Jinja template, records-search.html:

from flask import render_template

@app.route('/records-search')
def my_record_search():
    return render_template('records-search.html')

Then we need to extend search.html, inside our new templates/records-search.html template:

{# Contents of templates/records-search.html #}

{% extends 'invenio_search_ui/search.html' %}

{# We can also change here the title of the page #}
{% set title = 'My custom records search' %}
...

Next we’ll have to configure the <invenio-search> root directive with our endpoint and some additional querystring parameters:

...
{%- block body_inner -%}
<invenio-search
 search-endpoint="/api/records/"
 search-extra-params="{'type': 'article'}"
 search-hidden-params="{'status': 'published'}"
 search-headers="{'Accept': 'application/json'}"
>
{{super()}}
</invenio-search>
{%- endblock body_inner -%}
...

The URL that will be displayed to the user at the top of the search page will look something like (note the missing “hidden” parameter {'status': 'published'}):

https://myapp.org/search?type=article

Our requests to the REST API though will look something like this:

$ # The hidden parameter
$ curl -H "Accept: application/json" \
    "https://myapp.org/api/records?status=published&type=article"

Now let’s modify what is displayed in case our REST API request returns an error status code (4xx or 5xx). We do so by creating a new Angular template in static/templates/error.html and passing it to the template parameter of the <invenio-search-error> directive.

In our Jinja template records-search.html:

...
{%- block search_error -%}
<invenio-search-error
 template="{{ url_for('static', filename='templates/error.html') }}"
 message="{{ _('Search failed.') }}">
</invenio-search-error>
{%- endblock search_error -%}
...

In our new Angular template static/templates/error.html, we are going to add a link to some documentation page when a search error occurs:

<div ng-show='vm.invenioSearchError.name'>
  <div class="alert alert-danger">
    <strong>Error:</strong> {{vm.invenioSearchErrorResults.message || errorMessage }}
    <small><a href="https://myapp.org/help.html"></a></small>
  </div>
</div>

Let’s modify the way our search result items are displayed. In order to do so we need to create a static/templates/results.html template and update the directive’s template attribute.

In our Jinja template records-search.html:

...
{%- block search_results %}
<invenio-search-results
 template="{{ url_for('static', filename='templates/results.html') }}">
</invenio-search-results>
{%- endblock search_results %}

In our Angular template static/templates/results.html, we are going to display a link to the record’s actual page using the ng-href attribute and the links defined in the record.links object. We also display the creators of the record in a list by using the ng-repeat attribute and the records.metadata.creators array field.

<ul>
  <li ng-repeat="record in vm.invenioSearchResults.hits.hits track by $index">
    <a ng-href="record.links.self">
      <h5>{{ record.metadata.title }}</h5>
    </a>
    <ul>
      <li ng-repeat="creator in record.metadata.creators">{{creator.name}}</li>
    </ul>
    <p>{{ record.metadata.description }}</p>
  </li>
</ul>