Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sort ITIS response & multiple common names #237

Merged
merged 28 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
17f7b48
sort itis response to place exact matches first
mauberti-bc Mar 2, 2024
cb79c4e
helper functions for sorting itis response by exact and partial match
mauberti-bc Mar 2, 2024
b09cb31
cleanup sorting functions
mauberti-bc Mar 2, 2024
820854e
return first english common name
mauberti-bc Mar 2, 2024
6e575e3
simplify sorting
mauberti-bc Mar 2, 2024
352c17a
fix word matching
mauberti-bc Mar 2, 2024
85434b6
fix exact match sorting
mauberti-bc Mar 5, 2024
410dcd8
restore cache to 7 days
mauberti-bc Mar 5, 2024
f25fd6e
console logs
mauberti-bc Mar 5, 2024
978933b
fix tests
mauberti-bc Mar 5, 2024
2571f2d
common name from string to array
mauberti-bc Mar 5, 2024
7410677
cleanup
mauberti-bc Mar 5, 2024
b5fd23e
comments on itis sorting functions
mauberti-bc Mar 5, 2024
88e4b39
cleanup
mauberti-bc Mar 5, 2024
bdcc25a
undo commonName -> commonNames change in past migrations
mauberti-bc Mar 5, 2024
d772e39
fix placeholder tests
mauberti-bc Mar 5, 2024
70af144
addressing PR comments
mauberti-bc Mar 7, 2024
49b3f36
continue addressing PR comments
mauberti-bc Mar 7, 2024
d68c956
simplify itis sorting funcitons
mauberti-bc Mar 7, 2024
da9fa99
fix tests by changing scientific and common names to lowercase
mauberti-bc Mar 7, 2024
6090068
linter
mauberti-bc Mar 7, 2024
8fc01b0
JS Doc fix
mauberti-bc Mar 7, 2024
0793c88
Merge remote-tracking branch 'origin/dev' into search-adjustments
NickPhura May 1, 2024
1d6972c
Merge dev
NickPhura May 2, 2024
b85c5a2
Optimize sort query, had some minimal unit tests
NickPhura May 10, 2024
2717a93
Remove .only
NickPhura May 10, 2024
16a72a7
Better doc
NickPhura May 10, 2024
1dddfa5
Fix tests
NickPhura May 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/src/paths/taxonomy/taxon/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ describe('taxon', () => {

sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);

const mock1 = { id: '1', commonName: 'something', scientificName: 'string' } as unknown as any;
const mock2 = { id: '2', commonName: null, scientificName: 'string' } as unknown as any;
const mock1 = { id: '1', commonNames: ['something'], scientificName: 'string' } as unknown as any;
const mock2 = { id: '2', commonNames: null, scientificName: 'string' } as unknown as any;

const getSpeciesFromIdsStub = sinon.stub(ItisService.prototype, 'searchItisByTerm').resolves([mock1, mock2]);

Expand Down
16 changes: 12 additions & 4 deletions api/src/paths/taxonomy/taxon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,25 @@ GET.apiDoc = {
items: {
title: 'Taxon',
type: 'object',
required: ['tsn', 'commonName', 'scientificName'],
required: ['tsn', 'commonNames', 'scientificName'],
properties: {
tsn: {
type: 'integer'
},
commonName: {
type: 'string',
nullable: true
commonNames: {
type: 'array',
items: {
type: 'string'
}
},
scientificName: {
type: 'string'
},
rank: {
type: 'string'
},
kingdom: {
type: 'string'
}
},
additionalProperties: false
Expand Down
4 changes: 2 additions & 2 deletions api/src/paths/taxonomy/taxon/tsn/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ describe('tsn', () => {

sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);

const mock1 = { tsn: '1', commonName: 'something', scientificName: 'string' } as unknown as any;
const mock2 = { tsn: '2', commonName: null, scientificName: 'string' } as unknown as any;
const mock1 = { tsn: '1', commonNames: ['something'], scientificName: 'string' } as unknown as any;
const mock2 = { tsn: '2', commonNames: [], scientificName: 'string' } as unknown as any;

const getTaxonByTsnIdsStub = sinon.stub(TaxonomyService.prototype, 'getTaxonByTsnIds').resolves([mock1, mock2]);

Expand Down
16 changes: 12 additions & 4 deletions api/src/paths/taxonomy/taxon/tsn/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,25 @@ GET.apiDoc = {
items: {
title: 'Species',
type: 'object',
required: ['tsn', 'commonName', 'scientificName'],
required: ['tsn', 'commonNames', 'scientificName'],
properties: {
tsn: {
type: 'integer'
},
commonName: {
type: 'string',
nullable: true
commonNames: {
type: 'array',
items: {
type: 'string'
}
},
scientificName: {
type: 'string'
},
rank: {
type: 'string'
},
kingdom: {
type: 'string'
}
},
additionalProperties: false
Expand Down
2 changes: 1 addition & 1 deletion api/src/repositories/taxonomy-repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('TaxonomyRepository', () => {

const taxonomyRepository = new TaxonomyRepository(mockDBConnection);

const response = await taxonomyRepository.addItisTaxonRecord(1, 'string', 'string', {}, 'string');
const response = await taxonomyRepository.addItisTaxonRecord(1, 'string', ['string'], {}, 'string');

expect(response).to.be.eql({
taxon_id: 1,
Expand Down
7 changes: 4 additions & 3 deletions api/src/repositories/taxonomy-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class TaxonomyRepository extends BaseRepository {
*
* @param {number} itisTsn
* @param {string} itisScientificName
* @param {(string | null)} commonName
* @param {string[]} commonNames
* @param {Record<any, any>} itisData
* @param {string} itisUpdateDate
* @return {*} {Promise<TaxonRecord>}
Expand All @@ -62,12 +62,13 @@ export class TaxonomyRepository extends BaseRepository {
async addItisTaxonRecord(
itisTsn: number,
itisScientificName: string,
commonName: string | null,
commonNames: string[],
itisData: Record<string, unknown>,
itisUpdateDate: string
): Promise<TaxonRecord> {
defaultLog.debug({ label: 'addItisTaxonRecord', itisTsn });

// TODO: Store multiple common names rather than just the first item of the commonNames array
const sqlStatement = SQL`
WITH inserted_row AS (
INSERT INTO
Expand All @@ -82,7 +83,7 @@ export class TaxonomyRepository extends BaseRepository {
VALUES (
${itisTsn},
${itisScientificName},
${commonName},
${commonNames[0]},
${itisData},
${itisUpdateDate}
)
Expand Down
25 changes: 15 additions & 10 deletions api/src/services/itis-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@ describe('ItisService', () => {
response: {
docs: [
{
commonNames: ['$commonName'],
commonNames: ['$commonNames$English'],
kingdom: 'kingdom',
name: 'name',
parentTSN: 'parentTSN',
scientificName: 'scientificName',
tsn: '123',
updateDate: 'updateDate',
usage: 'usage'
usage: '',
rank: 'kingdom'
}
]
}
Expand All @@ -67,8 +68,10 @@ describe('ItisService', () => {
expect(response).to.eql([
{
tsn: 123,
commonName: 'commonName',
scientificName: 'scientificName'
commonNames: ['commonNames'],
scientificName: 'scientificName',
rank: 'kingdom',
kingdom: 'kingdom'
}
]);

Expand Down Expand Up @@ -121,14 +124,15 @@ describe('ItisService', () => {
response: {
docs: [
{
commonNames: ['$commonName'],
commonNames: ['$commonNames$English'],
kingdom: 'kingdom',
name: 'name',
parentTSN: 'parentTSN',
scientificName: 'scientificName',
tsn: '123',
updateDate: 'updateDate',
usage: 'usage'
usage: '',
rank: 'kingdom'
}
]
}
Expand All @@ -145,14 +149,15 @@ describe('ItisService', () => {

expect(response).to.eql([
{
commonNames: ['$commonName'],
commonNames: ['$commonNames$English'],
kingdom: 'kingdom',
name: 'name',
parentTSN: 'parentTSN',
scientificName: 'scientificName',
tsn: '123',
updateDate: 'updateDate',
usage: 'usage'
usage: '',
rank: 'kingdom'
}
]);

Expand Down Expand Up @@ -200,7 +205,7 @@ describe('ItisService', () => {
const response = await itisService.getItisSolrTermSearchUrl(['term']);

expect(response).to.equal(
'https://services.itis.gov/?wt=json&sort=nameWOInd+asc&rows=25&omitHeader=true&fl=tsn+scientificName:nameWOInd+kingdom+parentTSN+commonNames:vernacular+updateDate+usage&q=((nameWOInd:*term*+AND+usage:/(valid|accepted)/)+OR+(vernacular:*term*+AND+usage:/(valid|accepted)/))'
'https://services.itis.gov/?wt=json&sort=kingdom+asc&rows=150&omitHeader=true&fl=tsn+scientificName:nameWOInd+kingdom+parentTSN+commonNames:vernacular+updateDate+usage+rank&q=((nameWOInd:*term*+AND+usage:/(valid|accepted)/)+OR+(vernacular:*term*+AND+usage:/(valid|accepted)/))'
);
});
});
Expand Down Expand Up @@ -228,7 +233,7 @@ describe('ItisService', () => {
const response = await itisService.getItisSolrTsnSearchUrl([123]);

expect(response).to.equal(
'https://services.itis.gov/??wt=json&sort=nameWOInd+asc&rows=25&omitHeader=true&fl=tsn+scientificName:nameWOInd+kingdom+parentTSN+commonNames:vernacular+updateDate+usage&&q=tsn:123'
'https://services.itis.gov/??wt=json&sort=kingdom+asc&rows=150&omitHeader=true&fl=tsn+scientificName:nameWOInd+kingdom+parentTSN+commonNames:vernacular+updateDate+usage+rank&&q=tsn:123'
);
});
});
Expand Down
42 changes: 27 additions & 15 deletions api/src/services/itis-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios from 'axios';
import { sortTaxonSearchResults } from '../utils/itis-sort';
import { getLogger } from '../utils/logger';
import { TaxonSearchResult } from './taxonomy-service';

Expand All @@ -13,6 +14,7 @@ export type ItisSolrSearchResponse = {
tsn: string;
updateDate: string;
usage: string;
rank: string;
};

/**
Expand Down Expand Up @@ -42,7 +44,14 @@ export class ItisService {
return [];
}

return this._sanitizeItisData(response.data.response.docs);
const sanitizedResponse = this._sanitizeItisData(response.data.response.docs);

// Sort the results to place exact matches at the top
const sortedResponse = sortTaxonSearchResults(sanitizedResponse, searchTerms);

// Return only a subset of the records
// More records than are returned here are requested from ITIS to help find and prioritize exact matches
return sortedResponse.slice(0, 15);
}

/**
Expand All @@ -67,19 +76,22 @@ export class ItisService {
}

/**
* Cleans up the ITIS search response data.
* Cleans up the ITIS search response data
*
* @param {ItisSolrSearchResponse[]} data
* @memberof ItisService
*/
_sanitizeItisData = (data: ItisSolrSearchResponse[]): TaxonSearchResult[] => {
return data.map((item: ItisSolrSearchResponse) => {
const commonName = item.commonNames ? item.commonNames[0].split('$')[1] : null;
const englishNames = item.commonNames?.filter((name) => name.split('$')[2] === 'English');
const commonNames = englishNames?.map((name) => name.split('$')[1]) ?? [];

return {
tsn: Number(item.tsn),
commonName: commonName,
scientificName: item.scientificName
commonNames: commonNames,
scientificName: item.scientificName,
rank: item.rank,
kingdom: item.kingdom
};
});
};
Expand All @@ -100,9 +112,9 @@ export class ItisService {
}

return `${itisUrl}?${this._getItisSolrTypeParam()}&${this._getItisSolrSortParam(
'nameWOInd',
'asc',
25
['kingdom'],
['asc'],
150
)}&${this._getItisSolrFilterParam()}&${this._getItisSolrQueryParam(searchTerms)}`;
}

Expand All @@ -122,9 +134,9 @@ export class ItisService {
}

return `${itisUrl}??${this._getItisSolrTypeParam()}&${this._getItisSolrSortParam(
'nameWOInd',
'asc',
25
['kingdom'],
['asc'],
150
)}&${this._getItisSolrFilterParam()}&&q=${this._getItisSolrTsnSearch(searchTsnIds)}`;
}

Expand Down Expand Up @@ -153,12 +165,12 @@ export class ItisService {
*
* @param {string} sortBy
* @param {('asc' | 'desc')} sortDir
* @param {number} [limit=25]
* @param {number} limit
* @return {*} {string}
* @memberof ItisService
*/
_getItisSolrSortParam(sortBy: string, sortDir: 'asc' | 'desc', limit = 25): string {
return `sort=${sortBy}+${sortDir}&rows=${limit}`;
_getItisSolrSortParam(sortBy: string[], sortDir: ('asc' | 'desc')[], limit: number): string {
return `sort=${sortBy.map((f, index) => `${f}+${sortDir[index]}`).join(',')}&rows=${limit}`;
}

/**
Expand All @@ -168,7 +180,7 @@ export class ItisService {
* @memberof ItisService
*/
_getItisSolrFilterParam(): string {
return 'omitHeader=true&fl=tsn+scientificName:nameWOInd+kingdom+parentTSN+commonNames:vernacular+updateDate+usage';
return 'omitHeader=true&fl=tsn+scientificName:nameWOInd+kingdom+parentTSN+commonNames:vernacular+updateDate+usage+rank';
}

/**
Expand Down
15 changes: 8 additions & 7 deletions api/src/services/taxonomy-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ describe('TaxonomyService', () => {

const getItisSolrSearchResponse: ItisSolrSearchResponse[] = [
{
commonNames: ['$commonName'],
commonNames: ['$commonNames'],
kingdom: 'kingdom',
name: 'name',
parentTSN: 'parentTSN',
scientificName: 'scientificName',
tsn: 'tsn',
updateDate: 'updateDate',
usage: 'usage'
usage: 'usage',
rank: 'rank'
}
];

Expand Down Expand Up @@ -63,7 +64,7 @@ describe('TaxonomyService', () => {
const response = await taxonomyService.getTaxonByTsnIds([1]);

expect(repo).to.be.calledOnce;
expect(response).to.be.eql([{ tsn: 1, commonName: 'common_name', scientificName: 'itis_scientific_name' }]);
expect(response).to.be.eql([{ tsn: 1, commonNames: ['common_name'], scientificName: 'itis_scientific_name' }]);
});

it('if some records do not exist in db should return array of taxon records', async () => {
Expand Down Expand Up @@ -118,8 +119,8 @@ describe('TaxonomyService', () => {
expect(searchItisByTSNStub).to.be.calledOnce;
expect(itisService).to.be.calledOnce;
expect(response).to.be.eql([
{ tsn: 1, commonName: 'common_name', scientificName: 'itis_scientific_name' },
{ tsn: 2, commonName: 'common_name', scientificName: 'itis_scientific_name' }
{ tsn: 1, commonNames: ['common_name'], scientificName: 'itis_scientific_name' },
{ tsn: 2, commonNames: ['common_name'], scientificName: 'itis_scientific_name' }
]);
});
});
Expand All @@ -135,7 +136,7 @@ describe('TaxonomyService', () => {
itis_tsn: 1,
bc_taxon_code: null,
itis_scientific_name: 'scientificName',
common_name: 'commonName',
common_name: 'commonNames',
itis_data: {},
record_effective_date: 'updateDate',
record_end_date: null,
Expand All @@ -154,7 +155,7 @@ describe('TaxonomyService', () => {
itis_tsn: 1,
bc_taxon_code: null,
itis_scientific_name: 'scientificName',
common_name: 'commonName',
common_name: 'commonNames',
itis_data: {},
record_effective_date: 'updateDate',
record_end_date: null,
Expand Down
Loading
Loading