Part of the challenge of the BRUCE project is to take a highly relational model like CERIF and convert it into something which can be adequately indexed for searching and faceting.
Apache Solr, like many traditional search engines, works on the principle of key-value pairs. A key-value pair is simply an assertion that some value (on the right) is associated with some key (on the left). Examples of key-value pairs are:
name : Richard
project : bruce
organisation : Brunel University
Typically, the keys on the left come from a set of known terms, while the values on the right can vary arbitrarily. Therefore, when you search for documents belonging to “Richard”, you are asking which documents have the value “Richard” associated with the key “name”.
In addition, keys are often repeatable (although depending on the search index schema this might not be always the case), so you could have multiple “name” keys, with different values.
Approach
The objective, then, is for us to convert the graph-like structure of CERIF (that is, it has entities and relationships which do not follow a hierarchy) into the flat key-value structure of a search index. It should be clear from the outset, therefore, that data-loss will necessarily result from this conversion; it is not possible to fully and adequately represent a graph as a set of key-value pairs.
The project aimed, instead, to extract the key information from the CERIF schema from the point of view of one of the Base Entities.
There are 3 Base Entities in CERIF: Publications, People and Organisational Units. Since BRUCE is concerned with reporting principally on staff, we selected People as the Base Entity from which we would view the CERIF graph. By doing this we reduce the complexity of the challenge, since a graph viewed from the point of view of one of its nodes behaves like a hierarchy at least in the immediate vicinity (see the real analysis of this, below, for a clear example).
Our challenge is then simplified to representing a tree structure as a set of key-value pairs.
The second trick we need to use is to decide what kind of information we want to actually report on, and narrow our indexing to fields in the CERIF schema which are relevant to those requirements. This allows us to index values which are actually closely related to eachother as totally separate key-value pairs: as long as the index provides enough information for searching and faceting, it won’t matter that information about their relationship to eachother is lost.
For example: suppose we want to index the publications associated with a person, and we want to be able to list those publications as well as providing an integer count of how many publications were published by that person in some time frame. Initially this might look quite difficult, as a “publication” is a collection of related pieces of information, such as the title, the other authors, the date of publication, and other administrative terms such as page counts and so on. To place this in a set of key-value pairs would require us to do something like:
title: My Publication
publication_date: 01-09-2008
pages: 10
This is fine if there is only one publication by the person, but if they have multiple publications it would not be possible to tell which publication_date was associated with which title.
Instead, we have to remember that this is an index and not a data store. If we wish to list publication titles and count publications within date ranges, then it is just necessary for us to index the titles and the dates separately and ensure that they are used separately within the index. So we may have:
title: My First Paper
title: My Second Paper
publication_date: 01-09-2008
publication_date: 23-05-2009
This configuration loses data by not maintaining the links between publication_date and title, but is completely adequate for the indexing and faceting requirements.
To meet our original requirement stated above we can just count the number of publication_date keys which contain a date which lies within our desired time frame and return this integer count, while simultaneously listing the titles of the publication. The fact that these two pieces of information are not related in the index makes no difference in producing the desired outcome.
CERIF schema
The CERIF schema that we are working with is a limited sub-set of the project, and has been presented in a previous post. The set of tables which describe the graph contain the following fields that we are interested in are:
| CERIF Table |
Columns |
| cfPers |
cfPersId, cfGender |
| cfPers_Class |
cfPersId, cfClassSchemeId, cfClassId |
| cfPersName |
cfPersId, cfFirstNames, cfOtherNames, cfFamilyNames |
| cfPers_ResPubl |
cfPersId, cfResPublId, cfClassSchemeId, cfClassId |
| cfPers_OrgUnit |
cfPersId, cfOrgUnitId, cfClassSchemeId, cfClassId, cfFraction, cfEndDate |
| cfPers_Pers |
cfPersId1, cfPersId2, cfClassSchemeId, cfClassId |
| cfPers_Fund |
cfPersId, cfFundId |
| cfFund |
cfFundId, cfCurrCode, cfAmount |
| cfOrgUnit |
cfOrgUnitId, cfHeadcount |
| cfOrgUnitName |
cfOrgUnitId, cfName |
| cfOrgUnit_OrgUnit |
cfOrgUnitId1, cfOrgUnitId2, cfClassSchemeId, cfClassId |
| cfResPubl |
cfResPublId, cfResPublDate |
| cfResPublTitle |
cfResPublId, cfTitle |
| cfResPubl_Class |
cfResPublId, cfClassSchemeId, cfClassId |
Next, imagine that we pick up the graph by cfPers using cfPersId as the identifier which relates the person to all the other entities, and we can see that a rough hierarchy emerges:
cfPersId
cfGender
cfClassSchemeId
cfClassId
cfFirstNames
cfOtherNames
cfFamilyNames
cfResPublId
cfClassSchemeId
cfClassId
cfResPublDate
cfTitle
cfOrgUnitId
cfClassSchemeId
cfClassId
cfFraction
cfEndDate
cfHeadcount
cfName
cfOrgUnitId2**
cfFundId
cfCurrCode
cfAmount
With the exception of the Org Unit data (marked with **), the result is a straightforward enough hierarchy. We can avoid considering the graph that emerges under the organisation unit data by ensuring that the cfPers_OrgUnit table contains all the relevant relationships that we want to consider during indexing, so that we don’t have to attempt to index the org unit graph when preparing an index from the perspective of the person.
Solr index
The Solr index allows us to specify a field name (the key, in the key-value pair), and whether that field is repeatable or not. Each set of key-value pairs is grouped together into a “document”, and that document will represent a single person in the CERIF dataset, along with all the relevant data associated with them. When we have fully built our index, there will be one document per person.
The Solr index which then meets our requirements is constructed from the above CERIF data as follows:
| Field |
Single/Multi |
Value |
Notes |
| entity |
single |
“cfPers” |
Indicates that this is a person oriented document. This allows us to extend the index to view other kinds of entities as well, all represented within one schema. |
| id |
unique |
cfPersId |
A unique id representing the entity. When other entities are included in the index, this could also be their ids (e.g. cfResPublId) |
| gender |
single |
cfGender |
|
| name |
single |
a combination of cfFirstNames, cfOtherNames and cfFamilyNames |
This is the first person name that is encountered in the database, and is used for sorting and presented as the authors actual name. There is another field for name variants |
| name_variants |
multi |
a combination of cfFirstNames, cfOtherNames and cfFamilyNames |
This allows us to have multiple names for the author for the purposes of searching, although they will not be used for sorting or presented to the end user |
| contract_end |
single |
cfOrgUnit/cfEndDate |
Taken from the cfEndDate field in the cfPers_OrgUnit table which is tagged by cfClassId as Employee |
| funding_code |
multi |
cfFundId |
|
| org_unit_name |
multi |
cfOrgUnit/cfName |
|
| org_unit_id |
multi |
cfOrgUnit/cfOrgUnitId |
|
| primary_department |
single |
cfOrgUnit/cfName |
This differs from org_unit_name in that it is the department that the person should be considered most closely affiliated with. This would be, for example, their department or research group. It is used specifically for display and sorting, which is why it may only be single valued. |
| primary_department_id |
single |
cfOrgUnit/cfOrgUnitId |
The id for the department contained in primary_department |
| primary_position |
single |
cfOrgUnit/cfClassId |
The position that the person holds in their primary department (e.g. “Lecturer”) |
| fte |
single |
cfOrgUnit/cfFraction |
The fraction of the time that the person works for their organisational unit which is tagged with cfClassId of Employee. |
| supervising |
multi |
cfPers_Pers/cfPersId2 |
This lists the ids of the people that the person is supervising. These can be identified as the cfPers_Pers relationship has a cfClassId of Supervising |
| publication_date |
multi |
cfResPubl/cfResPublDate |
This lists the dates upon which the person published any result publications. This is a catch-all for all types of publication. Individual publication types are broken down in the following index fields |
| publication_id |
multi |
cfResPubl/cfResPublId |
This lists the ids of all the publications of any kind which the person published. |
| journal_date |
multi |
cfResPubl/cfResPublDate |
This is the list of dates of publication of all publications which have a cfClassId of “Journal Article”. |
| journal_id |
multi |
cfResPubl/cfResPublDate |
This is the list of ids publications which have a cfClassId of “Journal Article”. |
| book_date |
multi |
cfResPubl/cfResPublDate |
This is the list of dates of publication of all publications which have a cfClassId of “Book”. |
| book_id |
multi |
cfResPubl/cfResPublDate |
This is the list of ids publications which have a cfClassId of “Book”. |
| chapter_date |
multi |
cfResPubl/cfResPublDate |
This is the list of dates of publication of all publications which have a cfClassId of “Inbook”. |
| chapter_id |
multi |
cfResPubl/cfResPublDate |
This is the list of ids publications which have a cfClassId of “Inbook”. |
| conference_date |
multi |
cfResPubl/cfResPublDate |
This is the list of dates of publication of all publications which have a cfClassId of “Conference Proceedings Article”. |
| conference_id |
multi |
cfResPubl/cfResPublDate |
This is the list of ids publications which have a cfClassId of “Conference Proceedings Article”. |
These terms are encoded in a formal schema for Solr which can be found here.
Data Import
Apache Solr provides what it calls “Data Import Handlers” which allow you to import data from different kinds of sources into the index. Once we have configured the index as per the previous section we can construct a Data Import Handler which will import from the CERIF MySQL database.
This is effectively a set of SQL queries which are used to populate the index fields in the ways described in the previous section. A representitive example of the kinds of query include:
SELECT cfPers.cfPersId, cfPers.cfGender, 'cfPers' AS entity
FROM cfPers
INNER JOIN cfPers_Class
ON cfPers.cfPersId = cfPers_Class.cfPersId
WHERE cfPers_Class.cfClassSchemeId = 'BRUCE'
AND cfPers_Class.cfClassId = 'Main';
This query is at the root of the Data Import Handler, and selects our cfPersId which will be the central identifier that we will use to retrieve all other information, as well as any information which we can quickly and easily obtain by performing a JOIN operation across the cfPers* tables.
SELECT concat(cfFamilyNames, ', ', cfFirstNames, ' ', cfOtherNames) AS cfName
FROM cfPersName
WHERE cfPersId = '${person.cfPersId}'
LIMIT 1;
This query selects the first person’s name and performs the appropriate concatenation to turn the three name parts cfFamilyNames, cfFirstNames and cfOtherNames into a usable single string.
SELECT cfEndDate
FROM cfPers_OrgUnit
WHERE cfPersId = '${person.cfPersId}'
AND cfClassId = 'Employee'
AND cfClassSchemeId = 'cfCERIFSemantics_2008-1.2';
This query selects the person’s contract end date by looking for the organisational unit to which the person’s relationship (cfPers_OrgUnit) is annotated with the cfClassId ‘Employee’.
SELECT cfResPubl.cfResPublId, cfResPubl.cfResPublDate
FROM cfResPubl
INNER JOIN cfPers_ResPubl
ON cfPers_ResPubl.cfResPublId = cfResPubl.cfResPublId
INNER JOIN cfResPubl_Class
ON cfResPubl.cfResPublId = cfResPubl_Class.cfResPublId
WHERE cfPers_ResPubl.cfPersId = '${person.cfPersId}'
AND cfResPubl_Class.cfClassSchemeId = 'cfCERIFSemantics_2008-1.2'
AND cfResPubl_Class.cfClassId = 'Journal Article';
This query selects the ids and dates of publications by the selected person which have a class of ‘Journal Article’.
Here we will not go into this at any further length; instead the code which provides the Data Import functionality can be obtained here.
It is probably worth noting, though, that these queries are quite long and involve JOINing across multiple database tables, which makes reporting on the data hard work if done directly from source. The BRUCE approach means that this is all compressed into one single Data Import Handler, and leaves all the exciting stuff to the much simpler search engine query.
Use of the index
Once we have produced the index, we feed it into SolrEyes (discussed in more detail here) which is configured to produce the following functionality based on the indexed values:
| Field |
Usage |
| entity |
facet |
| id |
unused, required for index only |
| gender |
facet, result display |
| name |
sort, result display |
| name_variants |
currently unused |
| contract_end |
facet, sort, result display |
| funding_code |
result display |
| org_unit_name |
currently unused |
| org_unit_id |
currently unused |
| primary_department |
sort, result display |
| primary_department_id |
currently unused |
| primary_position |
facet, result display |
| fte |
facet, sort, result display |
| supervising |
result display (a function presents the number of people being supervised by the person) |
| publication_date |
facet, result display (a function counts the number of publications in between the date ranges specified by the facet) |
| publication_id |
currently unused |
| journal_date |
result display (a function counts the number of journal articles in between the date ranges specified by the publication_date facet) |
| journal_id |
currently unused |
| book_date |
result display (a function counts the number of journal articles in between the date ranges specified by the publication_date facet) |
| book_id |
currently unused |
| chapter_date |
result display (a function counts the number of journal articles in between the date ranges specified by the publication_date facet) |
| chapter_id |
currently unused |
| conference_date |
result display (a function counts the number of journal articles in between the date ranges specified by the publication_date facet) |
| conference_id |
currently unused |
Key:
- facet
- used to create the faceted browse navigation
- result display
- used when presenting a “document” to the user. Sometimes the value is a function of the actual indexed content.
- sort
- used for sorting the result set
Note that a more thorough treatment of the Solr index would split the fields up into multiple indexed fields which are customised for their purposes, but that we have not done this in the prototype. For example, fields used for sorting will go through normalising functions to ensure consistent sorting across all values, while displayable values will be stored unprocessed.
We can now produce a user interface like that shown in the screen shot below.

The approach used here could be used to extend to more features of the person Base Entity, but also other Base Entities (and, indeed, any entity in the CERIF model) could be placed at the centre of the report, and its resulting hierarchy of properties mapped into a set of key-value pairs, and all could co-exist comfortably in the same search index.