Migrating FieldIndexers in Jira 11

Still need help?

The Atlassian Community is here for you.

Ask the community

If your app uses a FieldIndexer through the customfield-searcher element, you need to update its configuration to align with the new search API. This update will prepare your app for future search tool enhancements that do not rely on Lucene. For a refresher on customfield-searcher, explore the custom field searcher configuration guide.

This page highlights the key modifications to indexers in the search API to assist you in updating your app. For more information, refer to the Search API upgrade guide

We're actively working on the upgrade documentation and will continue to update this article. Additionally, we will communicate any changes in our changelog to make tracking them easier. Explore the changelog

On this page:

Indexers

Indexers that use the Search API are designed to be platform agnostic. To ensure compatibility across multiple platforms, the following field definition restrictions apply:

  • Fields must be pre-defined and registered as part of the index schema.
  • Field names must be unique.

Starting with Jira Data Center 11, com.atlassian.jira.issue.index.indexers.FieldIndexer is replaced by com.atlassian.jira.search.issue.index.indexers.FieldIndexer. The table below provides method mappings for clear reference and implementation guidance.

Legacy Lucene
@com.atlassian.jira.issue.
index.indexers.FieldIndexer

Search API @com.atlassian.jira.search.issue.
index.indexers.FieldIndexer

Notes

String getId();

String getId();

Returns the ID of the Jira field that this indexer is indexing. This must be unique for each FieldIndexer.

If the indexer doesn't represent a System or Custom field in Jira, this must still return a unique string that describes the indexer.

String getDocumentFieldId();

Collection<Field> getFields();

Defines the fields on the index schema to configure how those fields will be stored and indexed on Lucene (and future search platforms).

void addIndex(Document doc, Issue issue)

void indexFields(FieldValueCollector collector, Issue issue, CustomFieldPrefetchedData prefetchedData);


void addIndex(Document doc, Issue issue, CustomFieldPrefetchedData prefetchedData)

void indexFields(FieldValueCollector collector, Issue issue, CustomFieldPrefetchedData prefetchedData);


boolean isFieldVisibleAndInScope(Issue issue);

boolean isFieldVisibleAndInScope(Issue issue);

To maintain existing behavior, the Lucene implementation of Search API won't index fields that are invisible.

Boolean skipsIndexingNull()

Boolean skipsIndexingNull()


To create a custom field indexer, ensure you implement the new FieldIndexer interface or extend one of its base classes. Additionally, make sure all fields have unique names and field IDs.

  1. Define a field. In this example, we'll use 'currency'.
  2. Add this field to the index schema.
  3. Index the field.
Example: How to implement a custom FieldIndexer

Legacy Lucene:

public class CurrencyFieldIndexer implements FieldIndexer {
    private final CustomField customField;
    
    public CurrencyFieldIndexer(CustomField customField) {
        this.customField = customField;
    }
    @Override
    public String getId() {
        return customField.getId();
    }
    @Override
    public String getDocumentFieldId() {
        return customField.getId();
    }
    @Override
    public void addIndex(Document doc, Issue issue, CustomFieldPrefetchedData prefetchedData) {
        final String currency = (String) prefetchedData.getData().orElseGet(() -> customField.getValue(issue));
        doc.add(new StringField(getDocumentFieldId(), currency, Field.Store.YES));
        doc.add(new SortedDocValuesField(getDocumentFieldId(), new BytesRef(currency)));
    }
    @Override
    public boolean isFieldVisibleAndInScope(Issue issue) {
       return true;
    }
}
public class CurrencyFieldIndexer extends BaseFieldIndexer {
    private final CustomField customField;
    
    public CurrencyFieldIndexer(CustomField customField, FieldVisibilityManager fieldVisibilityManager) {
        super(fieldVisibilityManager);
        this.customField = customField;
    }
    @Override
    public String getId() {
        return customField.getId();
    }
    @Override
    public String getDocumentFieldId() {
        return customField.getId();
    }
    @Override
    public void addIndex(Document doc, Issue issue) {
        final String currency = (String) prefetchedData.getData().orElseGet(() -> customField.getValue(issue));
        if (isFieldVisibleAndInScope(issue)) {
            doc.add(new StringField(getDocumentFieldId(), currency, Field.Store.YES));
            doc.add(new SortedSetDocValuesField(getDocumentFieldId(), new BytesRef(currency)));
        } else {
            doc.add(new StoredField(getDocumentFieldId(), currency));
        }       
    }
    @Override
    public boolean isFieldVisibleAndInScope(Issue issue) {
        /* 
         * This method only needs to be overridden if you need custom logic to determine
         * if the field should be visible. 
         * The base class BaseFieldIndexer handles verifying the field is visible on search queries
         * based upon the project permission schema.
         */
    }
}

Search API:

public class CurrencyFieldIndexer extends BaseCustomFieldIndexer {
    private final CustomField customField;
    private static KeywordField buildCustomField(final CustomField customField) {
        return KeywordField.builder(customField.getId())
                .indexed()
                .docValues()
                .stored()
                .build();
    }
    public CurrencyFieldIndexer(FieldVisibilityManager fieldVisibilityManager) {
        super(fieldVisibilityManager, customField, buildCustomField(customField));
        this.customField = customField;
    }
    @Override
    public void indexFields(FieldValueColloector collector, Issue issue, CustomFieldPrefetchedData prefetchedData) {
        final String currency = (String) prefetchedData.getData().orElseGet(() -> customField.getValue(issue));

        if (isFieldVisibleAndInScope(issue)) {
            collector.add(field.name(), currency);
        }
    }
    @Override
    public boolean isFieldVisibleAndInScope(Issue issue) {
        /* 
         * This method only needs to be overridden if you need custom logic to determine
         * if the field should be visible. 
         */
        return true;
    }
}


Search API to Lucene field mappings

Search API fields can be defined with the following properties:

  • Indexed: The field value is indexed and can be used for searching.
  • Stored: The original field value is stored and can be retrieved in a query result.
  • DocValues: The field can be used for non-query features such as sorting and aggregation.
  • MultiValues: A single document can have multiple values for the same field.

The Search API provides different types of fields for various data types and access patterns. To assist in migrating your app's Lucene fields to Search API fields, here is a description of how Jira maps Search API fields into the underlying Lucene index:

FieldMapping
KeywordField
  • Indexed → StringField
  • Not indexed, but Stored → StoredField
  • DocValues → SortedDocValuesField
  • DocValues & MultiValued → SortedSetDocValuesField

For example:

public static final KeywordField DOC_ID = KeywordField.builder("key")
        .multiValued()
        .indexed()
        .docValues()
        .stored()
        .build();

Results in Lucene fields:

  • StringField(name=“key”, stored=true)
  • SortedSetDocValuesField(name=”key”)

AnalyzedTextField

  • Indexed → TextField
  • Not Indexed, but Stored → StoredField

IntField

  • Indexed → IntField
  • Stored → StoredField
  • DocValues → NumericDocValuesField
  • DocValues & MultiValued → SortedNumericDocValuesField

LongField, DateTimeField

  • Indexed → LongPoint
  • Stored → StoredField
  • DocValues → NumericDocValuesField
  • DocValues & MultiValued → SortedNumericDocValuesField

DoubleField

  • Indexed → DoublePoint
  • Stored → StoredField
  • DocValues → DoubleDocValuesField
  • DocValues & MultiValued → SortedNumericDocValuesField

Maintaining backwards compatibility

Some configurations are possible in Lucene but not supported in OpenSearch, and therefore aren't part of the ongoing Search API. However, some mechanisms have been created to make this transition easier.

LuceneNameOverride

Lucene supports storing multiple different types of data with the same field name but Search API doesn’t support that. Due to this, we introduced a new method overrideLuceneName() in Jira 10.4 but it was marked as deprecated in the same version and completely removed in Jira 11, only being there for backwards compatibility.

Affected fields in Jira 11

As we've removed overrideLuceneName() in Jira 11, the following Jira fields have been affected:

Field name

Legacy Lucene field name

Search API field name

Product

Summary

pq_support_summary

summary.pq_support

Jira

Description

pq_support_description

description.pq_support

Environment

pq_support_environment

environment.pq_support

Comment body

pq_support_body

body.pq_support

Issue progress

progress

progress

progress.sort

Issue workratio

workratio

workratio

workratio.sort

Text Field custom field

pq_support_customfield_xxxxx

customfield_xxxxx.pq_support

Epic Label

customfield_xxxxx

customfield_xxxxx

JSW

sort_customfield_xxxxx

pq_support_customfield_xxxxx

customfield_xxxxx.pq_support

customfield_xxxxx.pq_support_sort

customfield_xxxxx_folded

customfield_xxxxx_folded

customfield_xxxxx_folded.sort

Satisfaction

customfield_xxxxx

customfield_xxxxx

JSM

customfield_xxxxx.sort

Organisation

customfield_xxxxx

customfield_xxxxx

customfield_xxxxx.sort

RetrievalName

By default, you can read the value of a field from a document using the field's name that was used to index the value. Specifying the retrieval name of a field allows you to use a different name. For example:

KeywordField textField = KeywordField.builder("searchApiFieldName")
        .retrievalName("oldFieldName")
        .build();
...
String documentValue = document.getString("oldFieldName");



Last modified on Jun 20, 2025

Was this helpful?

Yes
No
Provide feedback about this article
Powered by Confluence and Scroll Viewport.