Developer
Documentation
Resources
Get Support
Sign in
Developer
Get Support
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Developer
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Last updated Oct 17, 2025

Implementing custom query mappers

Available:

Jira 11.2 and later.

When developing Jira apps that need to perform custom search operations, you may need to create query mappers that can translate your custom query objects into the appropriate search engine queries. This is particularly important when supporting both Lucene and OpenSearch backends.

Before you begin

Before you create your query mappers, you need to have:

  • Jira Data Center 11.2 or later (for OpenSearch support).
  • understanding of Jira App Lifecycle.
  • basic knowledge of Lucene or OpenSearch query syntax.

Implementing custom query mappers in Jira

Step 1: Create your custom query interface

Create a query interface that extends com.atlassian.jira.search.Query.

1
2
package com.example.plugin.search.query;

import com.atlassian.jira.search.Query;

public interface RegexpQuery extends Query {
    String field();
    String value();
}

Step 2: Implement query interface

Create a concrete implementation of your query interface:

1
2
package com.example.plugin.search.query;

import java.util.Objects;
import static java.util.Objects.requireNonNull;

public class DefaultRegexpQuery implements RegexpQuery {
    private final String field;
    private final String value;

    public DefaultRegexpQuery(final String field, final String value) {
        this.field = requireNonNull(field);
        this.value = requireNonNull(value);
    }

    @Override
    public String field() {
        return field;
    }

    @Override
    public String value() {
        return value;
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == this) return true;
        if (obj == null || obj.getClass() != this.getClass()) return false;
        final var that = (DefaultRegexpQuery) obj;
        return Objects.equals(this.field, that.field) &&
                Objects.equals(this.value, that.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(field, value);
    }

    @Override
    public String toString() {
        return "DefaultRegexpQuery[" +
                "field=" + field + ", " +
                "value=" + value + ']';
    }
}

Step 3: Create query mapper

Depending on your search platform, implement the query mapper that translates your custom query to Lucene or OpenSeearch queries.

For Lucene, use the code below.

1
2
public class RegexpQueryMapper implements LuceneQueryMapper<RegexpQuery> {
    
    @Override
    public org.apache.lucene.search.RegexpQuery map(final RegexpQuery query) {
        return new org.apache.lucene.search.RegexpQuery(
            new org.apache.lucene.index.Term(query.field(), query.value())
        );
    }
}

For OpenSearch, use the code below.

1
2
public class RegexpQueryMapper implements OpenSearchQueryMapper<RegexpQuery> {
    @Override
    public Query map(final RegexpQuery query) {
        return org.opensearch.client.opensearch._types.query_dsl.Query.of(q -> q.regexp(r -> r
                .field(query.field())
                .value(query.value())
        ));
    }
}

Step 4: Register mapper

Depending on your search platform, register the mapper.

For Lucene, use the code below.

1
2
final LuceneQueryMapperRegistry registry = ComponentAccessor.getComponent(LuceneQueryMapperRegistry.class);
registry.registerMapper(RegexpQuery.class, this);

For OpenSearch, use the code below.

1
2
final OpenSearchQueryMapperRegistry registry = ComponentAccessor.getComponent(OpenSearchQueryMapperRegistry.class);
registry.registerMapper(RegexpQuery.class, this);

Step 5: Use your custom query

Here's an example of how to use your custom query in a search.

1
2
final IndexAccessorRegistry registry = ComponentAccessor.getComponent(IndexAccessorRegistry.class);
final IndexAccessor indexAccessor = registry.getIssuesIndexAccessor();

final SearchResponse response = indexAccessor.getSearcher().search(
        SearchRequest.builder()
                .query(new DefaultRegexpQuery(field, pattern))
                .documentType(DocumentTypes.ISSUE)
                .build(),
        PageRequest.of(0, 100));

App lifecycle integration

Component initialization and destruction

You can use the Jira App Lifecycle annotations to properly register and deregister query mappers:

  • @PostConstruct: Called during component initialization after dependency injection is complete. This is where you can register your mapper with the appropriate registry.
  • @PreDestroy: Called during component destruction when the app is being disabled or uninstalled. Make sure you clean up by deregistering your mapper.
1
2
@PostConstruct
public void onStarted() {
    final LuceneQueryMapperRegistry registry = ComponentAccessor.getComponent(LuceneQueryMapperRegistry.class);

    if (registry == null) {
        return;
    }

    registry.registerMapper(RegexpQuery.class, this);
}

@PreDestroy
public void onStopped() {
    final LuceneQueryMapperRegistry registry = ComponentAccessor.getComponent(LuceneQueryMapperRegistry.class);

    if (registry == null) {
        return;
    }

    registry.deregisterMapper(RegexpQuery.class);
}

Handling null registry

Null registry depends on the search platform:

  • When Jira runs with Lucene, the OpenSearchQueryMapperRegistry will be null.
  • When Jira runs with OpenSearch, the LuceneQueryMapperRegistry will be null.

Pay attention to the null registry because it allows your app to work seamlessly regardless of which search platform is active.

1
2
if (registry == null) {
    return;
}

Best practices

  • Always implement both mappers. Even if you only plan to support one search backend initially, implementing both ensures future compatibility.
  • Use proper lifecycle management and always register in @PostConstruct and deregister in @PreDestroy to prevent memory leaks.
  • Check for null registries to ensure your app works with both search backends.
  • Use consistent package structures and naming for your Lucene and OpenSearch implementations.
  • Ensure your custom queries work correctly with both Lucene and OpenSearch.
  • Your query implementations should have proper equality methods for caching and comparison.

Rate this page: