Migrating FieldIndexers in Jira 11

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;
    }
}


Registering fields on the index schema at runtime

Because Search API fields must be pre-defined and registered as part of the index schema, a FieldIndexer can only index values for field names the schema already knows about. Emitting a field name that isn't on the schema throws an InvalidDocumentFieldException and fails the document reindex job. This is deliberate: it surfaces a registration problem immediately, rather than letting it appear later as missing search results or broken JQL clauses.

In most cases you don't need to register anything yourself. When your indexer is reachable through a customfield-searcher, everything it returns from getFields() is collected into the issue index schema for you, and Jira rebuilds the schema whenever a custom field is created, updated, or deleted, or when the plugin module providing the searcher is enabled or disabled. Returning every field your indexer writes to from getFields() is all that's required.

You only need to register fields explicitly when they can't be discovered from a registered FieldIndexer at the time Jira builds the schema, for example, when your app's field names are only known at runtime, or when your app adds an indexer after the schema has already been built. For these cases, use IndexAccessor.updateSchema(...):

  public class CurrencyFieldSchemaRegistrar {
      private final IndexAccessorRegistry indexAccessorRegistry;
      public CurrencyFieldSchemaRegistrar(final IndexAccessorRegistry indexAccessorRegistry) {
          this.indexAccessorRegistry = indexAccessorRegistry;
      }
      public void registerCurrencyFields() {
          indexAccessorRegistry.getIssuesIndexAccessor().updateSchema(this::addCurrencyFields);
      }
      private IndexSchema addCurrencyFields(final IndexSchema existingSchema) {
          final IndexSchema.Builder builder = existingSchema.toBuilder();
          if (!builder.fieldIsDefined("currency")) {
              builder.addField(KeywordField.builder("currency")
                      .indexed()
                      .docValues()
                      .stored()
                      .build());
          }
          return builder.build();
      }
  }

Keep the following in mind:

  • Always start from the schema you're given. Build the new schema with existingSchema.toBuilder() so you keep the fields registered by Jira and by other apps, then add your own. Never construct a schema from scratch.

  • Field names are unique across the index. The schema is keyed by field name, so adding a field with a name already in use replaces the existing definition and Jira logs a warning. Guard with fieldIsDefined() and namespace your field names so they can't collide with another app's e.g. com.mycompany.myapp.fields.currencyfield.

  • A new field is picked up by the underlying search platform straight away, but documents indexed before the change aren't rewritten. Changing the type or properties of a field that's already in use requires a reindex.

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 Aug 12, 2026

Was this helpful?

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