Tutorials / Sending and managing data / Search Jira data
Nov. 06, 2019

Search Jira Data

Introduction

Indexing data from Jira in Algolia is a convenient way to search through content like projects, issues, users, tasks, etc. In this tutorial, you will learn how to quickly import and synchronize Jira objects in Algolia, using Node.js:

  • Getting an Atlassian API token
  • Fetching all projects from the Jira API
  • Indexing them to Algolia

For this, we’ll use the Jira Cloud Rest API v3.

Prerequisites

Familiar with Node.js

This tutorial assumes you are familiar with Node.js, how it works, and how to create and run Node.js scripts. If you want to learn more before going further, we recommend you read the following resources:

Also, you need to install Node.js in your environment.

Have a Jira and Algolia account

For this tutorial, we assume that you:

Install dependencies

You need to connect to your Algolia account. For that, you can use our Algolia Search library. We’ll also install Request to make HTTP calls.

Let’s add these dependencies to your project by running the following command in your terminal:

1
npm install algoliasearch request

Getting an Atlassian API token

To access the Jira Cloud REST API, you need to authenticate each HTTP request you make by including an Atlassian API token. You can create a new API token by following the instructions from Atlassian.

Fetching all projects from JIRA

Now that you have your token, you can make a request to the Jira Cloud REST API Get all projects endpoint.

1
2
3
4
5
6
7
8
9
10
11
12
13
const request = require('request');

const options = {
  method: 'GET',
  url: 'https://your-domain.atlassian.net/rest/api/3/project',
  auth: { username: 'email@example.com', password: 'your-atlassian-token' },
  headers: { Accept: 'application/json' }
};

request(options, (err, { statusCode } = {}, body) => {
  if (err || statusCode !== 200) throw err;
  const projects = JSON.parse(body);
});

Make sure you replace your-domain with your actual domain, email@example.com with your username, and your-atlassian-token with the API Token you generated earlier.

Importing your Jira projects into Algolia

Now that you’ve retrieved your Jira objects, you can create a script to index them into Algolia. You’ll likely want to be able to update and delete the project records later, so you need to specify an objectID for each of them:

1
2
3
const records = projects.map(project =>
  Object.assign({}, project, { objectID: project.id })
);

Now you can index your records array in Algolia:

1
2
3
4
5
6
7
8
const algoliasearch = require('algoliasearch');
const client = algoliasearch('YourApplicationID', 'YourAdminAPIKey');
const index = client.initIndex('YourIndexName');

index.addObjects(records, (err, content) => {
  if (err) throw err;
  console.log(content);
});

You just indexed your Jira projects into Algolia! You can already search them in your Algolia dashboard. You can also use Algolia’s Search UI Libraries to implement a front-end search experience and make your Jira data searchable anywhere you want.

Keeping Algolia in sync with Jira

Your Jira projects will change over time. For that reason, you may want to periodically fetch the latest data from Jira and update the corresponding records in your Algolia index. Algolia provides several ways to accomplish this.

Atomic reindexing

The first way to keep data in sync is to periodically re-import all records. You can do it by re-running the script from this tutorial (e.g., once a month), thus fetching all your Jira projects each time, formatting them and uploading them to Algolia. This process would overwrite all existing records in your index.

You need to adapt your existing script to achieve this process. You could add a bit of conditional logic, allowing the user to call the script with a flag to either index data for the first time, or atomically re-index on top of existing data.

For example, you could call your script with an --atomic flag:

1
node index.js --atomic

You can use the global process.argv property to access any user input params, and store a boolean to indicate whether your script should index normally or atomically.

1
const atomicFlag = process.argv.slice(2).includes('--atomic');

While much of your original script should remain unchanged, you want to be able to conditionally trigger the atomic re-indexing logic (for instance, as a separate function) whenever you pass the --atomic flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const atomicallyReindexData = (client, {indexName}, objects) => {
  // Instantiate temporary index
  const tmpIndex = client.initIndex('atomic_reindex_tmp');
  // Copy settings, synonyms, and Rules from original index to temporary index
  client.copyIndex(indexName, tmpIndex.indexName, [
      'settings',
      'synonyms',
      'rules'
  ]);
  // Add objects to temporary index
  while (objects.length) tmpIndex.addObjects(objects.splice(0, 1000));
  // Replace original index with temporary index
  client.moveIndex(tmpIndex.indexName, indexName);
}

For code hygiene, you can refactor your original indexing block into its own function as well:

1
2
3
4
5
6
7
const indexData = (index, data) => {
  // Index Jira project objects in Algolia
  index.addObjects(records, (err, content) => {
    if (err) throw err;
    console.log(content);
  });
}

Then, you can refactor the request logic of your original script to accommodate your new conditional check.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const start = () => {
  // Retrieve Jira projects
  request(options, (err, { statusCode } = {}, body) => {
    if (err || statusCode !== 200) throw err;
    const projects = JSON.parse(body);
    // Add object ID property to each project
    const records = projects.map(project =>
      Object.assign({}, project, { objectID: project.id })
    );
    // Index or re-index atomically depending on the use of --atomic flag
    if (atomicFlag) atomicallyReindexData(client, index, records);
    else indexData(index, records);
  });
}

This approach is usually the preferred one when you don’t have much data, or when you don’t update it frequently.

Incremental updates

The second way to keep data in sync is to selectively re-index records as the original objects change in Jira. In other words, if you update a Jira project, this would trigger a re-indexing of only that record into Algolia. To accomplish this, you can leverage the partialUpdateObjects method. Jira Webhooks could come in handy to set up such a workflow. To know more, read our incremental updates tutorial.

Batching records

Whatever strategy you pick, we recommend sending records in batches for optimal performances. To learn more about keeping data in sync, read our guides on synchronization.

Putting it all together

Gathering all above steps together, including atomic re-indexing, in a single script, you get something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const request = require('request');
const algoliasearch = require('algoliasearch');

const atomicFlag = process.argv.slice(2).includes('--atomic');

const client = algoliasearch('YourApplicationID', 'YourAdminAPIKey');
const index = client.initIndex('YourIndexName');

const options = {
  method: 'GET',
  url: 'https://your-domain.atlassian.net/rest/api/3/project',
  auth: { username: 'email@example.com', password: 'your-atlassian-token' },
  headers: { Accept: 'application/json' }
};

const start = () => {
  // Retrieve Jira projects
  request(options, (err, {statusCode}, body) => {
    if (err || statusCode !== 200) throw err;
    const projects = JSON.parse(body);
    // Add object ID property to each project
    const records = projects.map(project =>
      Object.assign({}, project, { objectID: project.id })
    );
    // Index or re-index atomically depending on the use of --atomic flag
    if (atomicFlag) atomicallyReindexData(client, index, records);
    else indexData(index, records);
  });
};

const indexData = (index, data) => {
  // Index Jira project objects in Algolia
  index.addObjects(records, (err, content) => {
    if (err) throw err;
    console.log(content);
  });
}

const atomicallyReindexData = (client, {indexName}, objects) => {
  // Instantiate temporary index
  const tmpIndex = client.initIndex('atomic_reindex_tmp');
  // Copy settings, synonyms, and Rules from original index to temporary index
  client.copyIndex(indexName, tmpIndex.indexName, [
      'settings',
      'synonyms',
      'rules'
  ]);
  // Add objects to temporary index
  while (objects.length) tmpIndex.addObjects(objects.splice(0, 1000));
  // Replace original index with temporary index
  client.moveIndex(tmpIndex.indexName, indexName);
}

start();

Conclusion

There are many ways to improve and go beyond this example. Here are some ideas:

Did you find this page helpful?