@natlibfi/melinda-record-matching 4.3.5 → 5.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/melinda-node-tests.yml +11 -7
- package/eslint.config.js +52 -0
- package/example.env +4 -0
- package/package.json +22 -90
- package/src/candidate-search/choose-queries.js +1 -3
- package/src/candidate-search/index.js +63 -89
- package/src/candidate-search/{index.spec.js → index.test.js} +19 -18
- package/src/candidate-search/query-list/bib.js +10 -4
- package/src/candidate-search/query-list/{bib.spec.js → bib.test.js} +8 -7
- package/src/candidate-search/query-list/component.js +127 -0
- package/src/candidate-search/query-list/index.js +23 -9
- package/src/cli.js +177 -0
- package/src/index.js +22 -37
- package/src/{index.spec.js → index.test.js} +11 -14
- package/src/match-detection/features/bib/all-source-ids.js +2 -2
- package/src/match-detection/features/bib/authors.js +3 -2
- package/src/match-detection/features/bib/index.js +16 -16
- package/src/match-detection/features/bib/{index.spec.js → index.test.js} +7 -6
- package/src/match-detection/features/bib/isbn.js +2 -2
- package/src/match-detection/features/bib/issn.js +1 -1
- package/src/match-detection/features/bib/language.js +5 -5
- package/src/match-detection/features/bib/media-type.js +1 -1
- package/src/match-detection/features/bib/melinda-id.js +1 -1
- package/src/match-detection/features/bib/melinda-identifier-factory.js +5 -5
- package/src/match-detection/features/bib/other-standard-identifier.js +1 -1
- package/src/match-detection/features/bib/publication-time-allow-cons-years-multi.js +6 -6
- package/src/match-detection/features/bib/publication-time-allow-cons-years.js +1 -1
- package/src/match-detection/features/bib/publication-time.js +1 -1
- package/src/match-detection/features/bib/standard-identifier-factory.js +1 -2
- package/src/match-detection/features/bib/title-version-original.js +2 -1
- package/src/match-detection/features/bib/title.js +6 -5
- package/src/match-detection/features/index.js +1 -1
- package/src/match-detection/index.js +1 -1
- package/src/match-detection/{index.spec.js → index.test.js} +9 -8
- package/src/matching-utils.js +8 -9
- package/dist/candidate-search/candidate-search-utils.js +0 -29
- package/dist/candidate-search/candidate-search-utils.js.map +0 -1
- package/dist/candidate-search/choose-queries.js +0 -122
- package/dist/candidate-search/choose-queries.js.map +0 -1
- package/dist/candidate-search/index.js +0 -260
- package/dist/candidate-search/index.js.map +0 -1
- package/dist/candidate-search/index.spec.js +0 -133
- package/dist/candidate-search/index.spec.js.map +0 -1
- package/dist/candidate-search/query-list/bib.js +0 -458
- package/dist/candidate-search/query-list/bib.js.map +0 -1
- package/dist/candidate-search/query-list/bib.spec.js +0 -46
- package/dist/candidate-search/query-list/bib.spec.js.map +0 -1
- package/dist/candidate-search/query-list/index.js +0 -47
- package/dist/candidate-search/query-list/index.js.map +0 -1
- package/dist/index.js +0 -555
- package/dist/index.js.map +0 -1
- package/dist/index.spec.js +0 -99
- package/dist/index.spec.js.map +0 -1
- package/dist/match-detection/features/bib/all-source-ids.js +0 -91
- package/dist/match-detection/features/bib/all-source-ids.js.map +0 -1
- package/dist/match-detection/features/bib/authors.js +0 -86
- package/dist/match-detection/features/bib/authors.js.map +0 -1
- package/dist/match-detection/features/bib/bibliographic-level.js +0 -15
- package/dist/match-detection/features/bib/bibliographic-level.js.map +0 -1
- package/dist/match-detection/features/bib/host-component.js +0 -15
- package/dist/match-detection/features/bib/host-component.js.map +0 -1
- package/dist/match-detection/features/bib/index.js +0 -119
- package/dist/match-detection/features/bib/index.js.map +0 -1
- package/dist/match-detection/features/bib/index.spec.js +0 -65
- package/dist/match-detection/features/bib/index.spec.js.map +0 -1
- package/dist/match-detection/features/bib/isbn.js +0 -49
- package/dist/match-detection/features/bib/isbn.js.map +0 -1
- package/dist/match-detection/features/bib/issn.js +0 -24
- package/dist/match-detection/features/bib/issn.js.map +0 -1
- package/dist/match-detection/features/bib/language.js +0 -113
- package/dist/match-detection/features/bib/language.js.map +0 -1
- package/dist/match-detection/features/bib/media-type.js +0 -56
- package/dist/match-detection/features/bib/media-type.js.map +0 -1
- package/dist/match-detection/features/bib/melinda-id.js +0 -21
- package/dist/match-detection/features/bib/melinda-id.js.map +0 -1
- package/dist/match-detection/features/bib/melinda-identifier-factory.js +0 -62
- package/dist/match-detection/features/bib/melinda-identifier-factory.js.map +0 -1
- package/dist/match-detection/features/bib/other-standard-identifier.js +0 -25
- package/dist/match-detection/features/bib/other-standard-identifier.js.map +0 -1
- package/dist/match-detection/features/bib/publication-time-allow-cons-years-multi.js +0 -229
- package/dist/match-detection/features/bib/publication-time-allow-cons-years-multi.js.map +0 -1
- package/dist/match-detection/features/bib/publication-time-allow-cons-years.js +0 -44
- package/dist/match-detection/features/bib/publication-time-allow-cons-years.js.map +0 -1
- package/dist/match-detection/features/bib/publication-time.js +0 -21
- package/dist/match-detection/features/bib/publication-time.js.map +0 -1
- package/dist/match-detection/features/bib/record-type.js +0 -18
- package/dist/match-detection/features/bib/record-type.js.map +0 -1
- package/dist/match-detection/features/bib/standard-identifier-factory.js +0 -188
- package/dist/match-detection/features/bib/standard-identifier-factory.js.map +0 -1
- package/dist/match-detection/features/bib/title-version-original.js +0 -52
- package/dist/match-detection/features/bib/title-version-original.js.map +0 -1
- package/dist/match-detection/features/bib/title.js +0 -70
- package/dist/match-detection/features/bib/title.js.map +0 -1
- package/dist/match-detection/features/index.js +0 -11
- package/dist/match-detection/features/index.js.map +0 -1
- package/dist/match-detection/index.js +0 -174
- package/dist/match-detection/index.js.map +0 -1
- package/dist/match-detection/index.spec.js +0 -63
- package/dist/match-detection/index.spec.js.map +0 -1
- package/dist/matching-utils.js +0 -115
- package/dist/matching-utils.js.map +0 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import createDebugLogger from 'debug';
|
|
2
|
+
import {promisify} from 'util';
|
|
3
|
+
import {toQueries} from '../candidate-search-utils.js';
|
|
4
|
+
import {getSubfieldValues, testStringOrNumber, toMelindaIds} from '../../matching-utils.js';
|
|
5
|
+
|
|
6
|
+
const setTimeoutPromise = promisify(setTimeout); // eslint-disable-line
|
|
7
|
+
|
|
8
|
+
export function hostIdMelinda(record) {
|
|
9
|
+
const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:hostIdMelinda');
|
|
10
|
+
const debugData = debug.extend('data'); // eslint-disable-line
|
|
11
|
+
debug(`Creating query for the Melinda Id host`);
|
|
12
|
+
|
|
13
|
+
const ids = hostId(record);
|
|
14
|
+
if (ids.length > 0) {
|
|
15
|
+
return toQueries(ids, 'melinda.partsofhost');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
debug(`No valid Melinda Id host found`);
|
|
19
|
+
return [];
|
|
20
|
+
|
|
21
|
+
function hostId(record) {
|
|
22
|
+
// Multi 773 handling
|
|
23
|
+
const f773s = record.get(/^773$/u)
|
|
24
|
+
.filter(f773 => f773.subfields.some(sub => sub.code === 'w' && (/\(FI-MELINDA\).*/ui).test(sub.value)));
|
|
25
|
+
|
|
26
|
+
if (f773s.length === 0) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Multi $w handling
|
|
31
|
+
// $w (prefix)<id> handling
|
|
32
|
+
// $w <id> & $w (prefix)<id> Match
|
|
33
|
+
const melindaIds = f773s.map(f773 => toMelindaIds(f773, ['w'])).flat()
|
|
34
|
+
.filter(value => testStringOrNumber(value)) // drop invalid values
|
|
35
|
+
.filter((value, index, array) => array.indexOf(value) === index); // unique values;
|
|
36
|
+
return melindaIds;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function hostIdOtherSource(record, client) {
|
|
41
|
+
const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:hostIdOtherSource');
|
|
42
|
+
const debugData = debug.extend('data'); // eslint-disable-line
|
|
43
|
+
debug(`Creating query for the Other source Id host`);
|
|
44
|
+
|
|
45
|
+
const ids = getHostIds(record);
|
|
46
|
+
const otherSources = getOtherSources(record);
|
|
47
|
+
if (ids.length > 0 && otherSources.length > 0) {
|
|
48
|
+
const otherSourceIds = ids.map(id => otherSources.map(source => `${id}${source}`)).flat();
|
|
49
|
+
const melindaHostQueries = await handleSruCalls(otherSourceIds);
|
|
50
|
+
debug(JSON.stringify(melindaHostQueries));
|
|
51
|
+
|
|
52
|
+
return melindaHostQueries;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
debug(`No valid other source hosts found`);
|
|
56
|
+
return [];
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
function getHostIds(record) {
|
|
60
|
+
const f773s = record.get(/^773$/u)
|
|
61
|
+
.filter(f773 => f773.subfields.some(sub => sub.code === 'w' && !(/\(FI-MELINDA\).*/ui).test(sub.value)));
|
|
62
|
+
if (f773s.length === 0) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Multi 773 handling
|
|
67
|
+
const subfieldWs = f773s
|
|
68
|
+
.map(f773 => {
|
|
69
|
+
debugData(`f773: ${JSON.stringify(f773)}`);
|
|
70
|
+
return getSubfieldValues(f773, 'w').flat();
|
|
71
|
+
}).flat();
|
|
72
|
+
|
|
73
|
+
// Multi $w handling
|
|
74
|
+
// $w (prefix)<id> handling
|
|
75
|
+
// $w <id> & $w (prefix)<id> Match
|
|
76
|
+
const ids = subfieldWs.map(value => `${value}`.replace(/\(FI-.*\)/ui, '')) // remove prefixes
|
|
77
|
+
.filter(value => testStringOrNumber(value)) // drop invalid values
|
|
78
|
+
.filter((value, index, array) => array.indexOf(value) === index); // unique values
|
|
79
|
+
|
|
80
|
+
return ids;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getOtherSources(record) {
|
|
84
|
+
const fSids = record.get('SID');
|
|
85
|
+
return fSids.map(field => getSubfieldValues(field, 'b'));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function handleSruCalls(otherSourceIds, ids = []) {
|
|
89
|
+
const [otherSourceId, ...rest] = otherSourceIds;
|
|
90
|
+
|
|
91
|
+
if (otherSourceId === undefined) {
|
|
92
|
+
debug(`host ids: ${ids}`);
|
|
93
|
+
const validIds = ids.filter(id => id);
|
|
94
|
+
return toQueries(validIds, 'melinda.partsofhost');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const otherSourceHostQuery = await toQueries([otherSourceId], 'melinda.sourceid');
|
|
98
|
+
const id = await new Promise((resolve, reject) => {
|
|
99
|
+
debug(`Searching for hosts with query: ${otherSourceHostQuery}`);
|
|
100
|
+
let recordId;
|
|
101
|
+
|
|
102
|
+
client.searchRetrieve(otherSourceHostQuery)
|
|
103
|
+
.on('error', err => {
|
|
104
|
+
debug(`SRU error for query: ${otherSourceHostQuery}: ${err}`);
|
|
105
|
+
reject(err);
|
|
106
|
+
})
|
|
107
|
+
.on('end', async () => {
|
|
108
|
+
try {
|
|
109
|
+
debug(`Searching for hosts: done`);
|
|
110
|
+
await setTimeoutPromise(10);
|
|
111
|
+
resolve(recordId);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
debug(`Error caught on END`);
|
|
114
|
+
reject(err);
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
.on('record', record => {
|
|
118
|
+
const [field] = record.get(/^001$/u);
|
|
119
|
+
debug(field);
|
|
120
|
+
recordId = field.value ? field.value : '';
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return handleSruCalls(rest, [...ids, id]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import * as bib from './bib';
|
|
1
|
+
import * as bib from './bib.js';
|
|
2
|
+
import * as component from './component.js';
|
|
4
3
|
import createDebugLogger from 'debug';
|
|
5
4
|
|
|
6
5
|
const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:index');
|
|
@@ -17,18 +16,33 @@ export const searchTypes = {
|
|
|
17
16
|
melindaId: 'bibMelindaIds',
|
|
18
17
|
sourceIds: 'bibSourceIds'
|
|
19
18
|
//DEVELOP: bibContent: 'bibContent'
|
|
19
|
+
},
|
|
20
|
+
component: {
|
|
21
|
+
hostIdMelinda: 'hostIdMelinda', // 773 $w (FI-MELINDA)
|
|
22
|
+
hostIdOtherSource: 'hostIdOtherSource', // 773 $w !(FI-MELINDA)
|
|
23
|
+
hostIsbn: 'hostIsbn' // 773 $z
|
|
20
24
|
}
|
|
21
25
|
};
|
|
22
26
|
|
|
23
|
-
export default (record, searchSpec) => {
|
|
24
|
-
const extractors = {...bib};
|
|
27
|
+
export default async (record, searchSpec, client) => {
|
|
28
|
+
const extractors = {...bib, ...component};
|
|
25
29
|
debugData(`extractors: ${JSON.stringify(extractors)}`);
|
|
26
30
|
debugData(`searchSpec: ${JSON.stringify(searchSpec)}`);
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const qExtractors = searchSpec.map(generateQueryExtractor);
|
|
33
|
+
const results = await handleQextractors(qExtractors);
|
|
34
|
+
return results;
|
|
35
|
+
|
|
36
|
+
async function handleQextractors(qExtractors, results = []) {
|
|
37
|
+
const [qExtractor, ...rest] = qExtractors;
|
|
38
|
+
|
|
39
|
+
if (qExtractor === undefined) {
|
|
40
|
+
return results.flat();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = await qExtractor(record, client);
|
|
44
|
+
return handleQextractors(rest, [...results, result]);
|
|
45
|
+
}
|
|
32
46
|
|
|
33
47
|
function generateQueryExtractor(type) {
|
|
34
48
|
if (extractors[type]) {
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import yargs from 'yargs';
|
|
3
|
+
import createMatchOperator, {candidateSearch, matchDetection} from './index.js';
|
|
4
|
+
import createDebugLogger from 'debug';
|
|
5
|
+
import {MarcRecord} from '@natlibfi/marc-record';
|
|
6
|
+
|
|
7
|
+
cli();
|
|
8
|
+
|
|
9
|
+
async function cli() {
|
|
10
|
+
const debug = createDebugLogger('@natlibfi/melinda-record-matching:cli');
|
|
11
|
+
const args = yargs(process.argv.slice(2))
|
|
12
|
+
.scriptName('melinda-record-matching-js')
|
|
13
|
+
.epilog('Copyright (C) 2022-2023 University Of Helsinki (The National Library Of Finland)')
|
|
14
|
+
.usage('$0 <file> [options] and env variable info in README')
|
|
15
|
+
.usage('Installed globally: $0 <file> [options] and env variable info in README')
|
|
16
|
+
.usage('Not installed: npx $0 <file> [options] and env variable info in README')
|
|
17
|
+
.usage('Build from source: node dist/index.js <file> [options] and env variable info in README')
|
|
18
|
+
.showHelpOnFail(true)
|
|
19
|
+
.example([['$ node /dist/cli.js record.json']])
|
|
20
|
+
.env('MELINDA_RECORD_MATCH')
|
|
21
|
+
.version()
|
|
22
|
+
.positional('file', {type: 'string', describe: 'Json file of records to match'})
|
|
23
|
+
.options({
|
|
24
|
+
t: {type: 'string', default: 'IDS', alias: 'searchType', describe: 'IDS, STANDARD_IDS, COMPONENT, CONTENT or CONTENTALT'},
|
|
25
|
+
m: {type: 'number', default: 1, alias: 'maxMatches', describe: ''},
|
|
26
|
+
c: {type: 'number', default: 1000, alias: 'maxCandidates', describe: ''},
|
|
27
|
+
s: {type: 'boolean', default: false, alias: 'returnStrategy', describe: ''},
|
|
28
|
+
q: {type: 'boolean', default: false, alias: 'returnQuery', describe: ''},
|
|
29
|
+
n: {type: 'boolean', default: false, alias: 'returnNonMatches', describe: ''}
|
|
30
|
+
})
|
|
31
|
+
.check((args) => {
|
|
32
|
+
const [file] = args._;
|
|
33
|
+
if (file === undefined) {
|
|
34
|
+
throw new Error('No file argument given');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!fs.existsSync(file)) {
|
|
38
|
+
throw new Error(`File ${file} does not exist`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (args.sruUrl === undefined) {
|
|
42
|
+
throw new Error('Setup sru url');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!['IDS', 'STANDARD_IDS', 'COMPONENT', 'CONTENT', 'CONTENTALT'].includes(args.searchType)) {
|
|
46
|
+
throw new Error('Invalid search type');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return true;
|
|
50
|
+
})
|
|
51
|
+
.parseSync();
|
|
52
|
+
|
|
53
|
+
const [file] = args._;
|
|
54
|
+
const {searchType} = args;
|
|
55
|
+
debug(JSON.stringify(args));
|
|
56
|
+
|
|
57
|
+
const detection = {
|
|
58
|
+
treshold: 0.9,
|
|
59
|
+
strategy: generateStrategy(searchType)
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const search = {
|
|
63
|
+
url: args.sruUrl, searchSpec: generateSearchSpec(searchType)
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const matchOperator = await createMatchOperator({detection, search, ...args});
|
|
67
|
+
|
|
68
|
+
const fileRaw = fs.readFileSync(file, 'utf8');
|
|
69
|
+
const record = new MarcRecord(JSON.parse(fileRaw), {subfieldValues: false});
|
|
70
|
+
|
|
71
|
+
const result = await matchOperator({record});
|
|
72
|
+
debug(JSON.stringify(result));
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
function generateStrategy(searchType) {
|
|
76
|
+
if (['IDS'].includes(searchType)) {
|
|
77
|
+
return [
|
|
78
|
+
matchDetection.features.bib.melindaId(),
|
|
79
|
+
matchDetection.features.bib.allSourceIds()
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// We could have differing strategy for STANDARD_IDS
|
|
84
|
+
// Let's not run title in strategy when we found the candidates through standard_ids search
|
|
85
|
+
|
|
86
|
+
if (['STANDARD_IDS'].includes(searchType)) {
|
|
87
|
+
return [
|
|
88
|
+
matchDetection.features.bib.hostComponent(),
|
|
89
|
+
matchDetection.features.bib.isbn(),
|
|
90
|
+
matchDetection.features.bib.issn(),
|
|
91
|
+
matchDetection.features.bib.otherStandardIdentifier(),
|
|
92
|
+
// Let's not use the same title matchDetection here
|
|
93
|
+
//matchDetection.features.bib.title(),
|
|
94
|
+
matchDetection.features.bib.authors(),
|
|
95
|
+
// We probably should have some leeway here for notated music as BK etc.
|
|
96
|
+
matchDetection.features.bib.recordType(),
|
|
97
|
+
// Use publicationTimeAllowConsYearsMulti to
|
|
98
|
+
// - ignore one year differences in publicationTime
|
|
99
|
+
// - extract publicationTimes from f008, f26x and reprint notes in f500
|
|
100
|
+
// - do not substract points for mismatching (normal) publicationTime, if there's a match between
|
|
101
|
+
// normal publicationTime and a reprintPublication time
|
|
102
|
+
matchDetection.features.bib.publicationTimeAllowConsYearsMulti(),
|
|
103
|
+
matchDetection.features.bib.language(),
|
|
104
|
+
matchDetection.features.bib.bibliographicLevel()
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (['COMPONENT'].includes(searchType)) {
|
|
109
|
+
return [
|
|
110
|
+
matchDetection.features.bib.hostComponent(),
|
|
111
|
+
matchDetection.features.bib.otherStandardIdentifier(),
|
|
112
|
+
matchDetection.features.bib.recordType(),
|
|
113
|
+
matchDetection.features.bib.title(),
|
|
114
|
+
matchDetection.features.bib.language(),
|
|
115
|
+
matchDetection.features.bib.authors(),
|
|
116
|
+
matchDetection.features.bib.bibliographicLevel()
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (['CONTENT', 'CONTENTALT'].includes(searchType)) {
|
|
121
|
+
return [
|
|
122
|
+
matchDetection.features.bib.hostComponent(),
|
|
123
|
+
matchDetection.features.bib.isbn(),
|
|
124
|
+
matchDetection.features.bib.issn(),
|
|
125
|
+
matchDetection.features.bib.otherStandardIdentifier(),
|
|
126
|
+
matchDetection.features.bib.title(),
|
|
127
|
+
matchDetection.features.bib.authors(),
|
|
128
|
+
matchDetection.features.bib.recordType(),
|
|
129
|
+
matchDetection.features.bib.publicationTime(),
|
|
130
|
+
matchDetection.features.bib.language(),
|
|
131
|
+
matchDetection.features.bib.bibliographicLevel()
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
throw new Error('Unsupported match validation package');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
function generateSearchSpec(searchType) {
|
|
140
|
+
if (['IDS'].includes(searchType)) {
|
|
141
|
+
return [
|
|
142
|
+
candidateSearch.searchTypes.bib.melindaId,
|
|
143
|
+
candidateSearch.searchTypes.bib.sourceIds
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (['STANDARD_IDS'].includes(searchType)) {
|
|
148
|
+
return [candidateSearch.searchTypes.bib.standardIdentifiers];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (['COMPONENT'].includes(searchType)) {
|
|
152
|
+
return [
|
|
153
|
+
//candidateSearch.searchTypes.bib.sourceIds,
|
|
154
|
+
candidateSearch.searchTypes.component.hostIdMelinda,
|
|
155
|
+
candidateSearch.searchTypes.component.hostIdOtherSource
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (['CONTENT'].includes(searchType)) {
|
|
160
|
+
return [
|
|
161
|
+
candidateSearch.searchTypes.bib.hostComponents,
|
|
162
|
+
//candidateSearch.searchTypes.bib.titleAuthor,
|
|
163
|
+
candidateSearch.searchTypes.bib.title
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
if (['CONTENTALT'].includes(searchType)) {
|
|
167
|
+
return [
|
|
168
|
+
candidateSearch.searchTypes.bib.hostComponents,
|
|
169
|
+
// titleAuthorYearAlternates searches for matchCandidates
|
|
170
|
+
// with alternate queries, starting from more tight searches
|
|
171
|
+
candidateSearch.searchTypes.bib.titleAuthorYearAlternates
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
throw new Error('Unsupported match validation package');
|
|
176
|
+
}
|
|
177
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import createDebugLogger from 'debug';
|
|
2
|
-
import createSearchInterface, * as candidateSearch from './candidate-search';
|
|
3
|
-
import createDetectionInterface, * as matchDetection from './match-detection';
|
|
2
|
+
import createSearchInterface, * as candidateSearch from './candidate-search/index.js';
|
|
3
|
+
import createDetectionInterface, * as matchDetection from './match-detection/index.js';
|
|
4
4
|
//import inspect from 'util';
|
|
5
5
|
|
|
6
6
|
export {candidateSearch, matchDetection};
|
|
7
7
|
|
|
8
|
-
export default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false
|
|
8
|
+
export default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false}) => {
|
|
9
9
|
const debug = createDebugLogger('@natlibfi/melinda-record-matching:index');
|
|
10
10
|
const debugData = debug.extend('data');
|
|
11
11
|
|
|
@@ -16,7 +16,6 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
16
16
|
debugData(`ReturnStrategy: ${JSON.stringify(returnStrategy)}`);
|
|
17
17
|
debugData(`ReturnQuery: ${JSON.stringify(returnQuery)}`);
|
|
18
18
|
debugData(`ReturnNonMatches: ${JSON.stringify(returnNonMatches)}`);
|
|
19
|
-
debugData(`ReturnFailures: ${JSON.stringify(returnFailures)}`);
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
const detect = createDetectionInterface(detectionOptions, returnStrategy);
|
|
@@ -41,17 +40,14 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
41
40
|
// state.queryCounter : sequence for current query
|
|
42
41
|
// state.maxedQueries : queries that resulted in more than serverMaxResults hits
|
|
43
42
|
|
|
44
|
-
async function iterate({initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0, nonMatchCount = 0,
|
|
43
|
+
async function iterate({initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0, nonMatchCount = 0, matchErrors = []}) {
|
|
45
44
|
debugData(`Starting next matcher iteration.`);
|
|
46
|
-
const {records,
|
|
45
|
+
const {records, ...state} = await search(initialState);
|
|
47
46
|
|
|
48
|
-
debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, nonMatchCount: ${nonMatchCount},
|
|
47
|
+
debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, nonMatchCount: ${nonMatchCount}, matchErrors: ${matchErrors.length}`);
|
|
49
48
|
const recordSetSize = records.length;
|
|
50
|
-
const
|
|
51
|
-
const newCandidateCount = candidateCount + recordSetSize + failureSetSize;
|
|
49
|
+
const newCandidateCount = candidateCount + recordSetSize;
|
|
52
50
|
|
|
53
|
-
const newConversionFailures = conversionFailures.concat(failures);
|
|
54
|
-
debugData(`Failures: ${failures.length}, ConversionFailures: ${conversionFailures.length}, NewConversionFailures: ${newConversionFailures.length}`);
|
|
55
51
|
|
|
56
52
|
if (recordSetSize > 0) {
|
|
57
53
|
return handleRecordSet();
|
|
@@ -59,11 +55,11 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
59
55
|
|
|
60
56
|
if (state.queriesLeft > 0) {
|
|
61
57
|
debug(`Empty record set ${state.searchCounter} for ${state.query}, but there are ${state.queriesLeft} queries left`);
|
|
62
|
-
return iterate({initialState: state, matches, candidateCount: newCandidateCount, nonMatches, nonMatchCount, duplicateCount,
|
|
58
|
+
return iterate({initialState: state, matches, candidateCount: newCandidateCount, nonMatches, nonMatchCount, duplicateCount, matchErrors});
|
|
63
59
|
}
|
|
64
60
|
|
|
65
61
|
debug(`No (more) candidate records to check, no more queries left, matches: ${matches.length}`);
|
|
66
|
-
return returnResult({matches, state, stopReason: '', nonMatches, nonMatchCount, candidateCount: newCandidateCount, duplicateCount,
|
|
62
|
+
return returnResult({matches, state, stopReason: '', nonMatches, nonMatchCount, candidateCount: newCandidateCount, duplicateCount, matchErrors});
|
|
67
63
|
|
|
68
64
|
function handleRecordSet() {
|
|
69
65
|
debug(`Checking record set of ${recordSetSize} candidate records for possible matches, found by ${state.searchCounter} search for ${state.query}`);
|
|
@@ -75,19 +71,18 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
75
71
|
const {newMatches, newNonMatches, newMatchErrors} = handleMatchResult(matchResult, matches, nonMatches, matchErrors);
|
|
76
72
|
|
|
77
73
|
if (maxMatchesFound({matches: newMatches, maxMatches})) {
|
|
78
|
-
return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount,
|
|
74
|
+
return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});
|
|
79
75
|
}
|
|
80
76
|
|
|
81
77
|
if (maxCandidatesRetrieved(newCandidateCount, maxCandidates)) {
|
|
82
|
-
return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount,
|
|
78
|
+
return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});
|
|
83
79
|
}
|
|
84
80
|
|
|
85
|
-
return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount,
|
|
81
|
+
return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});
|
|
86
82
|
}
|
|
87
83
|
|
|
88
84
|
function handleMatchResult(matchResult, matches, nonMatches, matchErrors) {
|
|
89
85
|
debugData(`- Amount of new matches from record set: ${matchResult.matches.length}`);
|
|
90
|
-
// eslint-disable-next-line functional/no-conditional-statements
|
|
91
86
|
if (returnNonMatches) {
|
|
92
87
|
debugData(`- Amount of new nonMatches from record set: ${matchResult.nonMatches.length}`);
|
|
93
88
|
}
|
|
@@ -97,7 +92,6 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
97
92
|
const newMatchErrors = matchErrors.concat(matchResult.matchErrors);
|
|
98
93
|
|
|
99
94
|
debugData(`- Total amount of matches: ${newMatches.length}`);
|
|
100
|
-
// eslint-disable-next-line functional/no-conditional-statements
|
|
101
95
|
if (returnNonMatches) {
|
|
102
96
|
debugData(`- Total amount of nonMatches: ${newNonMatches.length}`);
|
|
103
97
|
}
|
|
@@ -141,27 +135,24 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
141
135
|
// - only one stopReason is returned (if there would be several possible stopReasons, stopReason is picked in the above order)
|
|
142
136
|
// - currently stopReason can be non-empty also in cases where status is true, if matcher hit the stop reason when handling the last available candidate record
|
|
143
137
|
|
|
144
|
-
function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount, nonMatchCount,
|
|
145
|
-
const conversionFailureCount = conversionFailures.length;
|
|
138
|
+
function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount, nonMatchCount, matchErrors}) {
|
|
146
139
|
const matchErrorCount = matchErrors.length;
|
|
147
|
-
checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount,
|
|
148
|
-
const matchStatus = getMatchState(state, stopReason,
|
|
140
|
+
checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, matchErrorCount});
|
|
141
|
+
const matchStatus = getMatchState(state, stopReason, matchErrorCount);
|
|
149
142
|
// add nonMatches to result only if returnNonMatches is 'true', otherwise nonMatches have not been gathered
|
|
150
143
|
const matchesResult = returnNonMatches ? {matches, matchStatus, nonMatches, candidateCount} : {matches, matchStatus, candidateCount};
|
|
151
|
-
const
|
|
152
|
-
const result = returnFailures ? {...matchesResult, conversionFailures: failures} : matchesResult;
|
|
153
|
-
debugData(`ReturnFailures ${returnFailures}`);
|
|
144
|
+
const result = matchesResult;
|
|
154
145
|
debugData(`${JSON.stringify(result)}`);
|
|
155
146
|
return result;
|
|
156
147
|
|
|
157
148
|
// note that in cases where the matching has been stopped because of maxMatches checkCounts won't (in most cases) match
|
|
158
149
|
|
|
159
|
-
function checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount,
|
|
150
|
+
function checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, matchErrorCount}) {
|
|
160
151
|
const matchCount = matches.length;
|
|
161
152
|
debugData(`Return nonMatches: ${returnNonMatches}`);
|
|
162
153
|
const chosenNonMatchCount = returnNonMatches ? nonMatches.length : nonMatchCount;
|
|
163
154
|
const totalHandled = matchCount + chosenNonMatchCount + duplicateCount;
|
|
164
|
-
debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount},
|
|
155
|
+
debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}, matchErrorCount: ${matchErrorCount}`);
|
|
165
156
|
debug(`We got result for ${totalHandled} / ${candidateCount} retrieved candidates`);
|
|
166
157
|
if (totalHandled !== candidateCount) {
|
|
167
158
|
debug(`WARNING: Missing results for ${candidateCount - totalHandled} candidates`);
|
|
@@ -171,9 +162,8 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
171
162
|
}
|
|
172
163
|
|
|
173
164
|
// eslint-disable-next-line max-statements
|
|
174
|
-
function getMatchState(state, stopReason,
|
|
165
|
+
function getMatchState(state, stopReason, matchErrorCount) {
|
|
175
166
|
debugData(`${JSON.stringify(state)}`);
|
|
176
|
-
debug(`We had ${conversionFailuresCount} retrieved candidates that could not be converted.`);
|
|
177
167
|
debug(`We had ${matchErrorCount} retrieved candidates that errored in matchDetection.`);
|
|
178
168
|
debug(`Queries left ${state.queriesLeft}, Searches for current query left: ${state.resultSetOffset && state.resultSetOffset <= state.totalRecords}, non-retrieved records: ${state.totalRecords - state.queryCandidateCounter}, maxedQueries (${state.maxedQueries.length}): ${state.maxedQueries}`);
|
|
179
169
|
|
|
@@ -183,16 +173,14 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
183
173
|
const nonRetrieved = searchesLeft ? state.totalRecords - state.queryCandidateCounter : 0;
|
|
184
174
|
debugData(`nonRetrieved: ${nonRetrieved}`);
|
|
185
175
|
|
|
186
|
-
// matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries',
|
|
176
|
+
// matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries', empty string/undefined), reason for stopping retrieving or handling the candidate records
|
|
187
177
|
// 'maxMatches' and 'maxCandidates' are in stopReason, 'maxedQueries', 'conversionFailures' and 'matchErrors' are created here
|
|
188
178
|
|
|
189
|
-
if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0 ||
|
|
179
|
+
if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0 || matchErrorCount > 0) {
|
|
190
180
|
const maxedQueriesStopReason = state.maxedQueries.length > 0 ? 'maxedQueries' : undefined;
|
|
191
|
-
const conversionFailuresStopReason = conversionFailureCount > 0 ? 'conversionFailures' : undefined;
|
|
192
181
|
const matchErrorsStopReason = matchErrorCount > 0 ? 'matchErrors' : undefined;
|
|
193
|
-
const newStopReason = stopReason === '' || stopReason === undefined ? maxedQueriesStopReason ||
|
|
182
|
+
const newStopReason = stopReason === '' || stopReason === undefined ? maxedQueriesStopReason || matchErrorsStopReason : stopReason;
|
|
194
183
|
debugData(`MaxedQueriesStopReason: <${maxedQueriesStopReason}>`);
|
|
195
|
-
debugData(`ConversionFailureStopReason <${conversionFailuresStopReason}>`);
|
|
196
184
|
debugData(`MatchErrorsStopReason <${matchErrorsStopReason}>`);
|
|
197
185
|
debugData(`NewStopReason: <${newStopReason}>`);
|
|
198
186
|
debug(`Match status: false`);
|
|
@@ -240,8 +228,6 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
240
228
|
*/
|
|
241
229
|
|
|
242
230
|
if (candidate) {
|
|
243
|
-
|
|
244
|
-
// eslint-disable-next-line functional/no-conditional-statements
|
|
245
231
|
if (candidateNotInMatches(matches.concat(nonMatches), candidate)) {
|
|
246
232
|
const {record: candidateRecord, id: candidateId} = candidate;
|
|
247
233
|
const recordBExternal = {id: candidate.id, recordSource: 'databaseRecord', label: `db-${candidate.id}`};
|
|
@@ -304,7 +290,6 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
304
290
|
|
|
305
291
|
debugData(`- Total matches after this detection: ${matches.concat(newRecordMatches).length} (max: ${maxMatches})`);
|
|
306
292
|
|
|
307
|
-
// eslint-disable-next-line functional/no-conditional-statements
|
|
308
293
|
if (returnNonMatches) {
|
|
309
294
|
debugData(`- Total nonMatches after this detection: ${nonMatches.concat(newRecordNonMatches).length}`);
|
|
310
295
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import {describe} from 'node:test';
|
|
3
|
+
import createDebugLogger from 'debug';
|
|
2
4
|
import {READERS} from '@natlibfi/fixura';
|
|
3
5
|
import generateTests from '@natlibfi/fixugen-http-client';
|
|
4
6
|
import {MarcRecord} from '@natlibfi/marc-record';
|
|
5
|
-
import createMatchInterface, {matchDetection} from '.';
|
|
6
|
-
import createDebugLogger from 'debug';
|
|
7
|
+
import createMatchInterface, {matchDetection} from './index.js';
|
|
7
8
|
|
|
8
9
|
const debug = createDebugLogger('@natlibfi/melinda-record-matching:index:test');
|
|
9
10
|
const debugData = debug.extend('data');
|
|
@@ -11,7 +12,7 @@ const debugData = debug.extend('data');
|
|
|
11
12
|
describe('INDEX', () => {
|
|
12
13
|
generateTests({
|
|
13
14
|
callback,
|
|
14
|
-
path: [
|
|
15
|
+
path: [import.meta.dirname, '..', 'test-fixtures', 'index'],
|
|
15
16
|
recurse: false,
|
|
16
17
|
fixura: {
|
|
17
18
|
reader: READERS.JSON
|
|
@@ -30,19 +31,18 @@ describe('INDEX', () => {
|
|
|
30
31
|
const {matches, matchStatus, nonMatches, conversionFailures, candidateCount} = await match({record});
|
|
31
32
|
debugData(`Matches: ${matches.length}, Status: ${matchStatus.status}/${matchStatus.stopReason}, NonMatches: ${nonMatches ? nonMatches.length : 'not returned'}, ConversionFailures: ${conversionFailures ? conversionFailures.length : 'not returned'}`);
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
assert.equal(matchStatus.status, expectedMatchStatus);
|
|
35
|
+
assert.equal(matchStatus.stopReason, expectedStopReason);
|
|
36
|
+
assert.equal(candidateCount, expectedCandidateCount);
|
|
36
37
|
|
|
37
38
|
const formattedMatchResult = formatRecordResults(matches);
|
|
38
|
-
|
|
39
|
+
assert.deepStrictEqual(formattedMatchResult, expectedMatches);
|
|
39
40
|
|
|
40
41
|
const formattedNonMatchResult = formatRecordResults(nonMatches);
|
|
41
|
-
|
|
42
|
+
assert.deepStrictEqual(formattedNonMatchResult, expectedNonMatches);
|
|
42
43
|
|
|
43
|
-
// eslint-disable-next-line functional/no-conditional-statements
|
|
44
44
|
if (expectedFailures) {
|
|
45
|
-
|
|
45
|
+
assert.deepStrictEqual(conversionFailures, expectedFailures);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function formatOptions() {
|
|
@@ -80,8 +80,5 @@ describe('INDEX', () => {
|
|
|
80
80
|
record: newRecord.toObject()
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
83
|
}
|
|
86
|
-
|
|
87
84
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import createDebugLogger from 'debug';
|
|
3
|
-
import {validateSidFieldSubfieldCounts, getSubfieldValues} from '../../../matching-utils';
|
|
3
|
+
import {validateSidFieldSubfieldCounts, getSubfieldValues} from '../../../matching-utils.js';
|
|
4
4
|
|
|
5
5
|
const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:all-source-ids');
|
|
6
6
|
const debugData = debug.extend('data');
|
|
@@ -71,7 +71,7 @@ export default () => ({
|
|
|
71
71
|
debugData(`Comparing ${JSON.stringify(a)} and ${JSON.stringify(b)}`);
|
|
72
72
|
|
|
73
73
|
const matches = a.filter(sidA => b.some(sidB => sidA.sourceDb === sidB.sourceDb &&
|
|
74
|
-
|
|
74
|
+
sidA.sourceId === sidB.sourceId));
|
|
75
75
|
debugData(`Matches (${matches.length}): ${JSON.stringify(matches)}`);
|
|
76
76
|
|
|
77
77
|
const mismatches = a.filter(sidA => b.some(sidB => sidA.sourceDb === sidB.sourceDb &&
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
import naturalPkg from 'natural';
|
|
4
|
+
const {LevenshteinDistance: leven} = naturalPkg;
|
|
5
|
+
import {testStringOrNumber} from '../../../matching-utils.js';
|
|
5
6
|
|
|
6
7
|
// We should extract also organisational authors (110/710)
|
|
7
8
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
|
|
2
|
-
export {default as hostComponent} from './host-component';
|
|
3
|
-
export {default as isbn} from './isbn';
|
|
4
|
-
export {default as issn} from './issn';
|
|
5
|
-
export {default as otherStandardIdentifier} from './other-standard-identifier';
|
|
6
|
-
export {default as title} from './title';
|
|
7
|
-
export {default as titleVersionOriginal} from './title-version-original';
|
|
8
|
-
export {default as authors} from './authors';
|
|
9
|
-
export {default as recordType} from './record-type';
|
|
10
|
-
export {default as publicationTime} from './publication-time';
|
|
11
|
-
export {default as publicationTimeAllowConsYears} from './publication-time-allow-cons-years';
|
|
12
|
-
export {default as publicationTimeAllowConsYearsMulti} from './publication-time-allow-cons-years-multi';
|
|
13
|
-
export {default as language} from './language';
|
|
14
|
-
export {default as bibliographicLevel} from './bibliographic-level';
|
|
15
|
-
export {default as melindaId} from './melinda-id';
|
|
16
|
-
export {default as allSourceIds} from './all-source-ids';
|
|
17
|
-
export {default as mediaType} from './media-type';
|
|
2
|
+
export {default as hostComponent} from './host-component.js';
|
|
3
|
+
export {default as isbn} from './isbn.js';
|
|
4
|
+
export {default as issn} from './issn.js';
|
|
5
|
+
export {default as otherStandardIdentifier} from './other-standard-identifier.js';
|
|
6
|
+
export {default as title} from './title.js';
|
|
7
|
+
export {default as titleVersionOriginal} from './title-version-original.js';
|
|
8
|
+
export {default as authors} from './authors.js';
|
|
9
|
+
export {default as recordType} from './record-type.js';
|
|
10
|
+
export {default as publicationTime} from './publication-time.js';
|
|
11
|
+
export {default as publicationTimeAllowConsYears} from './publication-time-allow-cons-years.js';
|
|
12
|
+
export {default as publicationTimeAllowConsYearsMulti} from './publication-time-allow-cons-years-multi.js';
|
|
13
|
+
export {default as language} from './language.js';
|
|
14
|
+
export {default as bibliographicLevel} from './bibliographic-level.js';
|
|
15
|
+
export {default as melindaId} from './melinda-id.js';
|
|
16
|
+
export {default as allSourceIds} from './all-source-ids.js';
|
|
17
|
+
export {default as mediaType} from './media-type.js';
|