Writing to the Index
Atlassian has introduced a new Search API in Jira Data Center version 10.4, transitioning from Lucene to OpenSearch. As part of the Search API upgrade all Lucene indexers will be removed and need to be upgraded to Search API equivalents. Before the new Search API, when writing to the Index, we would rely on the Lucene indexers in this package:
In Search API, there is a new package with the Search API indexers:
This page focuses on explaining how to write to the index with the new Search API.
Key differences
Below we detail the key differences between the Lucene-based indexing approach and the new Search API implementation.
Changes to the index() method and introduction of FieldValueCollector to manipulate documents
Although most of the Lucene classes have equivalent classes in Search API (such as BooleanQuery being equivalent to DefaultBooleanQuery), the index() method has changed. This means that rather than relying on methods such as indexLocalDateField() or Document#add(), we rely on FieldValueCollector#add(). For any indexer, the signature of the index method has changed as follows:
index(Document, Issue) -> index(FieldValueCollector, Issue, CustomFieldPrefetchedData)Issuehas stayed the same.FieldValueCollectoris the new interface that is used to add fields to the document.CustomFieldPrefetchedDatais a collection of non-null custom fields on the iterated issue. All subclasses extendingFieldIndexerget access to this variable. The data type of the values varies per custom field.
Examples further down this page demonstrate how to refactor an existing indexer to use FieldValueCollector.
See Atlassian's documentation for more details.
Changes to field configuration and storage part of a field configuration
Previously, if you wanted to specify a given field configuration you would use an appropriate class provided by Lucene. For example, for stored-only fields or fields used for sorting:
doc.add(new StoredField(String name, BytesRef value))
or
doc.add(new SortedSetDocValuesField(documentFieldId, new BytesRef(String.valueOf(value.id))))With the new index() method, FieldValueCollector, and typed fields, you pick a subclass that extends AbstractField and then use it's builder to configure the field's specifications. To do so, we call either .stored() and/or .indexed() to configure the field to our use case.
Here is an example of an IntField that we want to be stored and indexed on a document:
groovydef exampleIntField = IntField.builder(STR_FIELD_NAME).stored().indexed().build() fieldValueCollector.add(exampleIntField, INT_NUMBER)
Or you could use FieldValueCollector#add(String fieldName, Object value) and provide a valid value to create a field with a default configuration:
groovyfieldValueCollector.add("fieldName", someValidValueSuchAsIntStrLongAndSoOn)
Refactoring examples
Below we provide examples of how to refactor your scripts to work with the new Search API.
Changing a typed Lucene field to work with Search API
In this example, we're using a StringField, which is a primitive typed Lucene field:
For reference, a typed Lucene field would use one of the primitive types such as StringField, IntField, LongField, etc.
groovy@Override void index(Document doc, Issue issue) { def stringValue = "value" doc.add(new StringField(getDocumentFieldId(), stringValue, Field.Store.YES)) }
Here's how to achieve the same result using the new Search API:
groovy@Override void indexFields(FieldValueCollector collector, Issue issue, CustomFieldPrefetchedData prefetchedData) { def stringValue = "value" def exampleField = KeywordField.builder("exampleName").stored().indexed().build() collector.add(exampleField, value) }
Refactoring AbstractCustomFieldIndexer subclasses
Classes that extend AbstractCustomFieldIndexer would likely have two overridden methods:
groovy@Override void addDocumentFieldsSearchable(Document doc, Issue issue) { // Logic to handle searchable field } @Override void addDocumentFieldsNotSearchable(Document doc, Issue issue) { // Logic to handle unsearchable field } OR if on a version <8.10 @Override void addDocumentFieldsSearchable(Document doc, Issue issue, CustomFieldPrefetchedData prefetchedData) { // Logic to handle searchable field } void addDocumentFieldsNotSearchable(Document doc, Issue issue, CustomFieldPrefetchedData prefetchedData) { // Logic to handle unsearchable field }
To be Search API compliant, we extend BaseCustomFieldIndexer and override the streamlined indexFields() method which handles both cases automatically:
groovyclass ExampleFieldIndexer extends BaseCustomFieldIndexer { protected ExampleFieldIndexer( FieldVisibilityManager fieldVisibilityManager, CustomField customField ) { super(fieldVisibilityManager, customField, KeywordField.builder("${customField.name}") .multiValued() .indexed() .docValues() .stored() .build()) } @Override void indexFields(FieldValueCollector collector, Issue issue, CustomFieldPrefetchedData prefetchedData) { def issueValue = this.getCustomField().getValue(issue) issueValue.each { String value -> if (value != null) { indexField(collector, value, issue) } } } }