@natlibfi/melinda-record-matching 2.2.0-alpha.1 → 2.2.0-alpha.4
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/dist/candidate-search/candidate-search-utils.js.map +1 -1
- package/dist/candidate-search/index.js +9 -9
- package/dist/candidate-search/index.js.map +1 -1
- package/dist/candidate-search/index.spec.js.map +1 -1
- package/dist/candidate-search/query-list/bib.js +2 -2
- package/dist/candidate-search/query-list/bib.js.map +1 -1
- package/dist/candidate-search/query-list/bib.spec.js.map +1 -1
- package/dist/candidate-search/query-list/index.js.map +1 -1
- package/dist/index.js +103 -33
- package/dist/index.js.map +1 -1
- package/dist/index.spec.js.map +1 -1
- package/dist/match-detection/features/bib/all-source-ids.js.map +1 -1
- package/dist/match-detection/features/bib/authors.js +1 -1
- package/dist/match-detection/features/bib/authors.js.map +1 -1
- package/dist/match-detection/features/bib/bibliographic-level.js.map +1 -1
- package/dist/match-detection/features/bib/host-component.js.map +1 -1
- package/dist/match-detection/features/bib/index.js.map +1 -1
- package/dist/match-detection/features/bib/index.spec.js.map +1 -1
- package/dist/match-detection/features/bib/isbn.js.map +1 -1
- package/dist/match-detection/features/bib/issn.js.map +1 -1
- package/dist/match-detection/features/bib/language.js.map +1 -1
- package/dist/match-detection/features/bib/melinda-id.js.map +1 -1
- package/dist/match-detection/features/bib/melinda-identifier-factory.js.map +1 -1
- package/dist/match-detection/features/bib/other-standard-identifier.js.map +1 -1
- package/dist/match-detection/features/bib/publication-time.js.map +1 -1
- package/dist/match-detection/features/bib/record-type.js.map +1 -1
- package/dist/match-detection/features/bib/standard-identifier-factory.js +3 -2
- package/dist/match-detection/features/bib/standard-identifier-factory.js.map +1 -1
- package/dist/match-detection/features/bib/title.js.map +1 -1
- package/dist/match-detection/features/index.js.map +1 -1
- package/dist/match-detection/index.js +36 -30
- package/dist/match-detection/index.js.map +1 -1
- package/dist/match-detection/index.spec.js +20 -2
- package/dist/match-detection/index.spec.js.map +1 -1
- package/dist/matching-utils.js +1 -1
- package/dist/matching-utils.js.map +1 -1
- package/package.json +13 -13
- package/src/candidate-search/index.js +11 -8
- package/src/candidate-search/query-list/bib.js +2 -2
- package/src/index.js +71 -30
- package/src/match-detection/features/bib/authors.js +1 -1
- package/src/match-detection/features/bib/standard-identifier-factory.js +2 -1
- package/src/match-detection/index.js +29 -19
- package/src/match-detection/index.spec.js +17 -3
- package/src/matching-utils.js +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"
|
|
1
|
+
{"version":3,"file":"matching-utils.js","names":["debug","createDebugLogger","debugData","extend","getMelindaIdsF035","record","f035s","getFields","length","allF035MelindaIds","concat","map","toMelindaIds","f035MelindaIds","Set","JSON","stringify","subfields","melindaIdRegExp","filter","sub","includes","code","test","value","replace","validateSidFieldSubfieldCounts","field","countC","countSubfields","countB","subfieldCode","getSubfieldValues"],"sources":["../src/matching-utils.js"],"sourcesContent":["/**\n*\n* @licstart The following is the entire license notice for the JavaScript code in this file.\n*\n* Melinda record matching modules for Javascript\n*\n* Copyright (C) 2020 University Of Helsinki (The National Library Of Finland)\n*\n* This file is part of melinda-record-matching-js\n*\n* melinda-record-matching-js program is free software: you can redistribute it and/or modify\n* it under the terms of the GNU Lesser General Public License as\n* published by the Free Software Foundation, either version 3 of the\n* License, or (at your option) any later version.\n*\n* melinda-record-matching-js is distributed in the hope that it will be useful,\n* but WITHOUT ANY WARRANTY; without even the implied warranty of\n* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n* GNU Lesser General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License\n* along with this program. If not, see <http://www.gnu.org/licenses/>.\n*\n* @licend The above is the entire license notice\n* for the JavaScript code in this file.\n*\n*/\n\nimport createDebugLogger from 'debug';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:utils');\nconst debugData = debug.extend('data');\n\n// eslint-disable-next-line max-statements\nexport function getMelindaIdsF035(record) {\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:melinda-id');\n const debugData = debug.extend('data');\n\n const f035s = record.getFields('035');\n\n if (f035s.length < 1) {\n debug(`No f035s found.`);\n return [];\n }\n\n const allF035MelindaIds = [].concat(...f035s.map(toMelindaIds));\n const f035MelindaIds = [...new Set(allF035MelindaIds)];\n\n debugData(`Fields (${f035s.length}): ${JSON.stringify(f035s)}`);\n debugData(`Ids (${allF035MelindaIds.length}): ${JSON.stringify(allF035MelindaIds)}`);\n debugData(`Unique ids (${f035MelindaIds.length}): ${JSON.stringify(f035MelindaIds)}`);\n\n return f035MelindaIds;\n\n function toMelindaIds({subfields}) {\n const melindaIdRegExp = /^(?<prefix>\\(FI-MELINDA\\)|FCC)(?<id>\\d{9})$/u;\n\n return subfields\n .filter(sub => ['a', 'z'].includes(sub.code))\n .filter(sub => melindaIdRegExp.test(sub.value))\n .map(({value}) => value ? value.replace(melindaIdRegExp, '$<id>') : '');\n\n }\n}\n\nexport function validateSidFieldSubfieldCounts(field) {\n // Valid SID-fields have just one $c and one $b\n debugData(`Validating SID field ${JSON.stringify(field)}`);\n const countC = countSubfields(field, 'c');\n const countB = countSubfields(field, 'b');\n debug(`Found ${countC} sf $cs and ${countB} sf $bs. IsValid: ${countC === 1 && countB === 1}`);\n\n return countC === 1 && countB === 1;\n}\n\nfunction countSubfields(field, subfieldCode) {\n // debug(`Counting subfields ${subfieldCode}`);\n return field.subfields.filter(({code}) => code === subfieldCode).length;\n}\n\nexport function getSubfieldValues(field, subfieldCode) {\n debugData(`Get subfield(s) $${subfieldCode} from ${JSON.stringify(field)}`);\n return field.subfields.filter(({code}) => code === subfieldCode).map(({value}) => value);\n}\n\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"url": "git@github.com:natlibfi/melinda-record-matching-js.git"
|
|
15
15
|
},
|
|
16
16
|
"license": "LGPL-3.0+",
|
|
17
|
-
"version": "2.2.0-alpha.
|
|
17
|
+
"version": "2.2.0-alpha.4",
|
|
18
18
|
"main": "./dist/index.js",
|
|
19
19
|
"engines": {
|
|
20
20
|
"node": ">=14"
|
|
@@ -40,20 +40,20 @@
|
|
|
40
40
|
"@natlibfi/melinda-commons": "^12.0.3",
|
|
41
41
|
"@natlibfi/sru-client": "^5.0.1",
|
|
42
42
|
"debug": "^4.3.4",
|
|
43
|
-
"moment": "^2.29.
|
|
44
|
-
"natural": "^5.
|
|
43
|
+
"moment": "^2.29.4",
|
|
44
|
+
"natural": "^5.2.3",
|
|
45
45
|
"uuid": "^8.3.2",
|
|
46
|
-
"winston": "^3.
|
|
46
|
+
"winston": "^3.8.1"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@babel/cli": "^7.
|
|
50
|
-
"@babel/core": "^7.
|
|
51
|
-
"@babel/eslint-parser": "^7.
|
|
52
|
-
"@babel/node": "^7.
|
|
53
|
-
"@babel/preset-env": "^7.
|
|
54
|
-
"@babel/register": "^7.
|
|
49
|
+
"@babel/cli": "^7.18.10",
|
|
50
|
+
"@babel/core": "^7.18.13",
|
|
51
|
+
"@babel/eslint-parser": "^7.18.9",
|
|
52
|
+
"@babel/node": "^7.18.10",
|
|
53
|
+
"@babel/preset-env": "^7.18.10",
|
|
54
|
+
"@babel/register": "^7.18.9",
|
|
55
55
|
"@natlibfi/eslint-config-melinda-backend": "^2.0.0",
|
|
56
|
-
"@natlibfi/fixugen": "^1.0
|
|
56
|
+
"@natlibfi/fixugen": "^1.1.0",
|
|
57
57
|
"@natlibfi/fixugen-http-client": "^2.0.1",
|
|
58
58
|
"@natlibfi/fixura": "^2.2.1",
|
|
59
59
|
"babel-plugin-istanbul": "^6.1.1",
|
|
@@ -61,9 +61,9 @@
|
|
|
61
61
|
"chai": "^4.3.6",
|
|
62
62
|
"chai-as-promised": "^7.1.1",
|
|
63
63
|
"cross-env": "^7.0.3",
|
|
64
|
-
"eslint": "^8.
|
|
64
|
+
"eslint": "^8.23.0",
|
|
65
65
|
"mocha": "^9.2.2",
|
|
66
|
-
"nodemon": "^2.0.
|
|
66
|
+
"nodemon": "^2.0.19",
|
|
67
67
|
"nyc": "^15.1.0"
|
|
68
68
|
},
|
|
69
69
|
"eslintConfig": {
|
|
@@ -66,6 +66,9 @@ export default ({record, searchSpec, url, maxCandidates, maxRecordsPerRequest =
|
|
|
66
66
|
|
|
67
67
|
debug(`Searching matches for ${inputRecordId}`);
|
|
68
68
|
debug(`Generated queryList ${JSON.stringify(queryList)}`);
|
|
69
|
+
|
|
70
|
+
// if generateQueryList errored we should throw 422
|
|
71
|
+
|
|
69
72
|
if (queryList.length === 0) { // eslint-disable-line functional/no-conditional-statement
|
|
70
73
|
throw new CandidateSearchError(`Generated query list contains no queries`);
|
|
71
74
|
}
|
|
@@ -149,21 +152,21 @@ export default ({record, searchSpec, url, maxCandidates, maxRecordsPerRequest =
|
|
|
149
152
|
reject(err);
|
|
150
153
|
}
|
|
151
154
|
})
|
|
152
|
-
.on('record',
|
|
155
|
+
.on('record', recordXML => {
|
|
153
156
|
promises.push(handleRecord()); // eslint-disable-line functional/immutable-data
|
|
154
157
|
|
|
155
158
|
async function handleRecord() {
|
|
156
159
|
try {
|
|
157
|
-
const
|
|
158
|
-
const
|
|
160
|
+
const recordMarc = await MARCXML.from(recordXML, {subfieldValues: false});
|
|
161
|
+
const recordId = getRecordId(recordMarc);
|
|
159
162
|
|
|
160
|
-
return {record:
|
|
163
|
+
return {record: recordMarc, id: recordId};
|
|
161
164
|
} catch (err) {
|
|
162
165
|
// What should this do?
|
|
163
|
-
const idFromXML = getRecordIdFromXML(
|
|
164
|
-
debugData(`Failed converting record: ${err.message}, id: ${idFromXML}, data: ${
|
|
165
|
-
//return {message: `Failed converting record: ${err.message}`, id: idFromXML, data:
|
|
166
|
-
throw new MatchingError(422, {message: `Failed converting record: ${err.message}`, id: idFromXML || '000000000', data:
|
|
166
|
+
const idFromXML = getRecordIdFromXML(recordXML);
|
|
167
|
+
debugData(`Failed converting record: ${err.message}, id: ${idFromXML}, data: ${recordXML}`);
|
|
168
|
+
//return {message: `Failed converting record: ${err.message}`, id: idFromXML, data: recordXML};
|
|
169
|
+
throw new MatchingError(422, {message: `Failed converting record: ${err.message}`, id: idFromXML || '000000000', data: recordXML});
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
172
|
});
|
|
@@ -108,7 +108,7 @@ export function bibSourceIds(record) {
|
|
|
108
108
|
|
|
109
109
|
function removeSourcePrefix(subfieldValue) {
|
|
110
110
|
const sourcePrefixRegex = (/^(?<sourcePrefix>\([A-Za-z0-9-]+\))(?<id>.+)$/u);
|
|
111
|
-
const normalizedValue = subfieldValue.replace(sourcePrefixRegex, '$<id>');
|
|
111
|
+
const normalizedValue = subfieldValue ? subfieldValue.replace(sourcePrefixRegex, '$<id>') : '';
|
|
112
112
|
debugData(`Normalized ${subfieldValue} to ${normalizedValue}`);
|
|
113
113
|
return normalizedValue;
|
|
114
114
|
}
|
|
@@ -116,7 +116,7 @@ export function bibSourceIds(record) {
|
|
|
116
116
|
function normalizeSidSubfieldValue(subfieldValue) {
|
|
117
117
|
debugData(`Normalizing ${subfieldValue}`);
|
|
118
118
|
const normalizeAwayRegex = (/[- ]/u);
|
|
119
|
-
return subfieldValue.replace(normalizeAwayRegex, '');
|
|
119
|
+
return subfieldValue ? subfieldValue.replace(normalizeAwayRegex, '') : '';
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
}
|
package/src/index.js
CHANGED
|
@@ -29,10 +29,11 @@
|
|
|
29
29
|
import createDebugLogger from 'debug';
|
|
30
30
|
import createSearchInterface, * as candidateSearch from './candidate-search';
|
|
31
31
|
import createDetectionInterface, * as matchDetection from './match-detection';
|
|
32
|
+
//import inspect from 'util';
|
|
32
33
|
|
|
33
34
|
export {candidateSearch, matchDetection};
|
|
34
35
|
|
|
35
|
-
export default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false, returnFailures}) => {
|
|
36
|
+
export default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false, returnFailures = false}) => {
|
|
36
37
|
const debug = createDebugLogger('@natlibfi/melinda-record-matching:index');
|
|
37
38
|
const debugData = debug.extend('data');
|
|
38
39
|
|
|
@@ -65,11 +66,11 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
65
66
|
// state.queryCounter : sequence for current query
|
|
66
67
|
// state.maxedQueries : queries that resulted in more than serverMaxResults hits
|
|
67
68
|
|
|
68
|
-
async function iterate({initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0, nonMatchCount = 0, conversionFailures = []}) {
|
|
69
|
+
async function iterate({initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0, nonMatchCount = 0, conversionFailures = [], matchErrors = []}) {
|
|
69
70
|
debugData(`Starting next matcher iteration.`);
|
|
70
71
|
const {records, failures, ...state} = await search(initialState);
|
|
71
72
|
|
|
72
|
-
debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, nonMatchCount: ${nonMatchCount}, conversionFailures: ${conversionFailures}`);
|
|
73
|
+
debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, nonMatchCount: ${nonMatchCount}, conversionFailures: ${conversionFailures}, matchErrors: ${matchErrors.length}`);
|
|
73
74
|
const recordSetSize = records.length;
|
|
74
75
|
const failureSetSize = failures.length;
|
|
75
76
|
const newCandidateCount = candidateCount + recordSetSize + failureSetSize;
|
|
@@ -83,11 +84,11 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
83
84
|
|
|
84
85
|
if (state.queriesLeft > 0) {
|
|
85
86
|
debug(`Empty record set ${state.searchCounter} for ${state.query}, but there are ${state.queriesLeft} queries left`);
|
|
86
|
-
return iterate({initialState: state, matches, candidateCount: newCandidateCount, nonMatches, nonMatchCount, duplicateCount, conversionFailures: newConversionFailures});
|
|
87
|
+
return iterate({initialState: state, matches, candidateCount: newCandidateCount, nonMatches, nonMatchCount, duplicateCount, conversionFailures: newConversionFailures, matchErrors});
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
debug(`No (more) candidate records to check, no more queries left, matches: ${matches.length}`);
|
|
90
|
-
return returnResult({matches, state, stopReason: '', nonMatches, nonMatchCount, candidateCount: newCandidateCount, duplicateCount, conversionFailures: newConversionFailures});
|
|
91
|
+
return returnResult({matches, state, stopReason: '', nonMatches, nonMatchCount, candidateCount: newCandidateCount, duplicateCount, conversionFailures: newConversionFailures, matchErrors});
|
|
91
92
|
|
|
92
93
|
function handleRecordSet() {
|
|
93
94
|
debug(`Checking record set of ${recordSetSize} candidate records for possible matches, found by ${state.searchCounter} search for ${state.query}`);
|
|
@@ -96,20 +97,20 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
96
97
|
|
|
97
98
|
const newDuplicateCount = duplicateCount + matchResult.duplicateCount;
|
|
98
99
|
const newNonMatchCount = nonMatchCount + matchResult.nonMatchCount;
|
|
99
|
-
const {newMatches, newNonMatches} = handleMatchResult(matchResult, matches, nonMatches);
|
|
100
|
+
const {newMatches, newNonMatches, newMatchErrors} = handleMatchResult(matchResult, matches, nonMatches, matchErrors);
|
|
100
101
|
|
|
101
102
|
if (maxMatchesFound({matches: newMatches, maxMatches})) {
|
|
102
|
-
return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures});
|
|
103
|
+
return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures, matchErrors: newMatchErrors});
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
if (maxCandidatesRetrieved(newCandidateCount, maxCandidates)) {
|
|
106
|
-
return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures});
|
|
107
|
+
return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures, matchErrors: newMatchErrors});
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures});
|
|
110
|
+
return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures, matchErrors: newMatchErrors});
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
function handleMatchResult(matchResult, matches, nonMatches) {
|
|
113
|
+
function handleMatchResult(matchResult, matches, nonMatches, matchErrors) {
|
|
113
114
|
debugData(`- Amount of new matches from record set: ${matchResult.matches.length}`);
|
|
114
115
|
// eslint-disable-next-line functional/no-conditional-statement
|
|
115
116
|
if (returnNonMatches) {
|
|
@@ -118,6 +119,7 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
118
119
|
|
|
119
120
|
const newMatches = matches.concat(returnQuery ? addQuery(matchResult.matches) : matchResult.matches);
|
|
120
121
|
const newNonMatches = returnNonMatches ? nonMatches.concat(returnQuery ? addQuery(matchResult.nonMatches) : matchResult.nonMatches) : [];
|
|
122
|
+
const newMatchErrors = matchErrors.concat(matchResult.matchErrors);
|
|
121
123
|
|
|
122
124
|
debugData(`- Total amount of matches: ${newMatches.length}`);
|
|
123
125
|
// eslint-disable-next-line functional/no-conditional-statement
|
|
@@ -125,7 +127,12 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
125
127
|
debugData(`- Total amount of nonMatches: ${newNonMatches.length}`);
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
|
|
130
|
+
debugData(`MatchResult: ${JSON.stringify(matchResult)}`);
|
|
131
|
+
debugData(`Old matchErrors: ${JSON.stringify(matchErrors)}, matchErrors from matchResult: ${JSON.stringify(matchResult.matchErrors)}, New matchErrors: ${JSON.stringify(newMatchErrors)}`);
|
|
132
|
+
|
|
133
|
+
debugData(`- Total amount of matchErrors: ${newMatchErrors.length}`);
|
|
134
|
+
|
|
135
|
+
return {newMatches, newNonMatches, newMatchErrors};
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
function addQuery(matches) {
|
|
@@ -150,6 +157,7 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
150
157
|
// - strategy (if returnStrategy option is true)
|
|
151
158
|
// - treshold (if returnStrategy option is true)
|
|
152
159
|
// - matchQuery (if returnQuery option is true)
|
|
160
|
+
// failures: array of conversionFailures from candidate-search and matchErrors from matchDetection in error format {status, payload: {message, id}} if returnFailures is true
|
|
153
161
|
|
|
154
162
|
// we could have here also returnRecords/returnMatchRecords/returnNonMatchRecord options that could be turned false for not to return actual record data
|
|
155
163
|
|
|
@@ -158,25 +166,27 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
158
166
|
// - only one stopReason is returned (if there would be several possible stopReasons, stopReason is picked in the above order)
|
|
159
167
|
// - 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
|
|
160
168
|
|
|
161
|
-
function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount, nonMatchCount, conversionFailures}) {
|
|
169
|
+
function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount, nonMatchCount, conversionFailures, matchErrors}) {
|
|
162
170
|
const conversionFailureCount = conversionFailures.length;
|
|
163
|
-
|
|
164
|
-
|
|
171
|
+
const matchErrorCount = matchErrors.length;
|
|
172
|
+
checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, conversionFailureCount, matchErrorCount});
|
|
173
|
+
const matchStatus = getMatchState(state, stopReason, conversionFailureCount, matchErrorCount);
|
|
165
174
|
// add nonMatches to result only if returnNonMatches is 'true', otherwise nonMatches have not been gathered
|
|
166
175
|
const matchesResult = returnNonMatches ? {matches, matchStatus, nonMatches} : {matches, matchStatus};
|
|
167
|
-
const
|
|
176
|
+
const failures = [...conversionFailures, ...matchErrors];
|
|
177
|
+
const result = returnFailures ? {...matchesResult, conversionFailures: failures} : matchesResult;
|
|
168
178
|
debugData(`ReturnFailures ${returnFailures}`);
|
|
169
179
|
debugData(`${JSON.stringify(result)}`);
|
|
170
180
|
return result;
|
|
171
181
|
|
|
172
182
|
// note that in cases where the matching has been stopped because of maxMatches checkCounts won't (in most cases) match
|
|
173
183
|
|
|
174
|
-
function checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, conversionFailureCount}) {
|
|
184
|
+
function checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, conversionFailureCount, matchErrorCount}) {
|
|
175
185
|
const matchCount = matches.length;
|
|
176
186
|
debugData(`Return nonMatches: ${returnNonMatches}`);
|
|
177
187
|
const chosenNonMatchCount = returnNonMatches ? nonMatches.length : nonMatchCount;
|
|
178
188
|
const totalHandled = matchCount + chosenNonMatchCount + duplicateCount;
|
|
179
|
-
debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}, conversionFailureCount: ${conversionFailureCount}`);
|
|
189
|
+
debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}, conversionFailureCount: ${conversionFailureCount}, matchErrorCount: ${matchErrorCount}`);
|
|
180
190
|
debug(`We got result for ${totalHandled} / ${candidateCount} retrieved candidates`);
|
|
181
191
|
if (totalHandled !== candidateCount) {
|
|
182
192
|
debug(`WARNING: Missing results for ${candidateCount - totalHandled} candidates`);
|
|
@@ -185,9 +195,11 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
185
195
|
return;
|
|
186
196
|
}
|
|
187
197
|
|
|
188
|
-
|
|
198
|
+
// eslint-disable-next-line max-statements
|
|
199
|
+
function getMatchState(state, stopReason, conversionFailuresCount, matchErrorCount) {
|
|
189
200
|
debugData(`${JSON.stringify(state)}`);
|
|
190
201
|
debug(`We had ${conversionFailuresCount} retrieved candidates that could not be converted.`);
|
|
202
|
+
debug(`We had ${matchErrorCount} retrieved candidates that errored in matchDetection.`);
|
|
191
203
|
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}`);
|
|
192
204
|
|
|
193
205
|
debugData(`StopReason: <${stopReason}>`);
|
|
@@ -197,14 +209,16 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
197
209
|
debugData(`nonRetrieved: ${nonRetrieved}`);
|
|
198
210
|
|
|
199
211
|
// matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries','conversionFailures', empty string/undefined), reason for stopping retrieving or handling the candidate records
|
|
200
|
-
// 'maxMatches' and 'maxCandidates' are in stopReason, 'maxedQueries' and '
|
|
212
|
+
// 'maxMatches' and 'maxCandidates' are in stopReason, 'maxedQueries', 'conversionFailures' and 'matchErrors' are created here
|
|
201
213
|
|
|
202
|
-
if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0 || conversionFailureCount > 0) {
|
|
214
|
+
if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0 || conversionFailureCount > 0 || matchErrorCount > 0) {
|
|
203
215
|
const maxedQueriesStopReason = state.maxedQueries.length > 0 ? 'maxedQueries' : undefined;
|
|
204
216
|
const conversionFailuresStopReason = conversionFailureCount > 0 ? 'conversionFailures' : undefined;
|
|
205
|
-
const
|
|
217
|
+
const matchErrorsStopReason = matchErrorCount > 0 ? 'matchErrors' : undefined;
|
|
218
|
+
const newStopReason = stopReason === '' || stopReason === undefined ? maxedQueriesStopReason || conversionFailuresStopReason || matchErrorsStopReason : stopReason;
|
|
206
219
|
debugData(`MaxedQueriesStopReason: <${maxedQueriesStopReason}>`);
|
|
207
220
|
debugData(`ConversionFailureStopReason <${conversionFailuresStopReason}>`);
|
|
221
|
+
debugData(`MatchErrorsStopReason <${matchErrorsStopReason}>`);
|
|
208
222
|
debugData(`NewStopReason: <${newStopReason}>`);
|
|
209
223
|
debug(`Match status: false`);
|
|
210
224
|
return {status: false, stopReason: newStopReason};
|
|
@@ -215,7 +229,13 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
215
229
|
}
|
|
216
230
|
}
|
|
217
231
|
|
|
218
|
-
|
|
232
|
+
// NOTES:
|
|
233
|
+
// - we could optimize by creating the featureSet for the incoming record once and using it for all database/candidateRecords
|
|
234
|
+
// - if creating the featureSet for the incoming record fails we have an unprocessable entity
|
|
235
|
+
// - if creating the featureSet for a candidate record fails we could skip that candidate - but list the case as a detectionFailure, same as conversionFailures
|
|
236
|
+
|
|
237
|
+
// eslint-disable-next-line max-statements
|
|
238
|
+
function iterateRecords({records, recordSetSize, maxMatches, matches = [], nonMatches = [], recordMatches = [], recordNonMatches = [], recordCount = 0, recordDuplicateCount = 0, recordNonMatchCount = 0, recordMatchErrors = []}) {
|
|
219
239
|
|
|
220
240
|
// recordSetSize : total amount of records in the current record set
|
|
221
241
|
// recordCount : amount of records from the current record set that have been handled
|
|
@@ -227,6 +247,7 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
227
247
|
// matches : found matches in the current matcher job
|
|
228
248
|
// recordMatches : found matches in the current record set
|
|
229
249
|
// recordNonMatches : found nonMatches in the current record set (only if returnNonMatches setting is true)
|
|
250
|
+
// recordMatchErrors: errored matchDetection in the current record set
|
|
230
251
|
|
|
231
252
|
const [candidate] = records;
|
|
232
253
|
const newRecordCount = candidate ? recordCount + 1 : recordCount;
|
|
@@ -236,19 +257,39 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
236
257
|
// Note that if returnNonMatches is false, matcher won't remember candidates that didn't match, so they will be matched again everytime they are retrieved by
|
|
237
258
|
// different candidate search queries. Same candidate search query won't have duplicate records.
|
|
238
259
|
|
|
260
|
+
/* We could optimize and detect all retrieved candidates at once
|
|
261
|
+
const candidateRecords = records.map(record => record.record);
|
|
262
|
+
const recordsIsArray = Array.isArray(candidateRecords);
|
|
263
|
+
debug(`records is an array: ${recordsIsArray}`);
|
|
264
|
+
const result = detect(record, candidateRecords);
|
|
265
|
+
debug(`${JSON.stringify(result)}`);
|
|
266
|
+
*/
|
|
267
|
+
|
|
239
268
|
if (candidate) {
|
|
240
269
|
|
|
270
|
+
// eslint-disable-next-line functional/no-conditional-statement
|
|
241
271
|
if (candidateNotInMatches(matches.concat(nonMatches), candidate)) {
|
|
242
272
|
const {record: candidateRecord, id: candidateId} = candidate;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
273
|
+
try {
|
|
274
|
+
debug(`Running matchDetection for record ${candidateId} (${newRecordCount}/${recordSetSize})`);
|
|
275
|
+
// we should handle errors from detection somehow - ie. cases where either record or candidateRecord errors
|
|
276
|
+
const detectionResult = detect(record, candidateRecord);
|
|
277
|
+
|
|
278
|
+
return handleDetectionResult(detectionResult, candidateId, candidateRecord);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
debug(`MatchDetection errored: database record ${candidateId}: ${error}`);
|
|
281
|
+
|
|
282
|
+
const matchError = {status: 422, payload: {message: `Matching errored for database record ${candidateId}. ${error.message}.`, id: candidateId}};
|
|
283
|
+
const newRecordMatchErrors = recordMatchErrors.concat(matchError);
|
|
284
|
+
return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount, recordMatchErrors: newRecordMatchErrors});
|
|
285
|
+
}
|
|
246
286
|
}
|
|
247
|
-
|
|
287
|
+
|
|
288
|
+
return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount: recordDuplicateCount + 1, recordNonMatchCount, recordMatchErrors});
|
|
248
289
|
}
|
|
249
290
|
|
|
250
291
|
debug(`No more candidates, record set (${recordCount}/${recordSetSize}) done, ${recordMatches.length} matches found, ${recordDuplicateCount} candidates already handled, ${returnNonMatches ? `${recordNonMatches.length}` : `${recordNonMatchCount}`} nonMatches found.`);
|
|
251
|
-
return {matches: recordMatches, nonMatches: returnNonMatches ? recordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: recordNonMatchCount};
|
|
292
|
+
return {matches: recordMatches, nonMatches: returnNonMatches ? recordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: recordNonMatchCount, matchErrors: recordMatchErrors};
|
|
252
293
|
|
|
253
294
|
function handleDetectionResult(detectionResult, candidateId, candidateRecord) {
|
|
254
295
|
debugData(`MatchDetection results for ${candidateId} (${newRecordCount}/${recordSetSize}): ${JSON.stringify(detectionResult)}`);
|
|
@@ -278,7 +319,7 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
278
319
|
const newRecordNonMatchCount = recordNonMatchCount + 1;
|
|
279
320
|
debugData(`- Total nonMatches after this detection: ${newRecordNonMatchCount}`);
|
|
280
321
|
|
|
281
|
-
return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount});
|
|
322
|
+
return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount, recordMatchErrors});
|
|
282
323
|
}
|
|
283
324
|
|
|
284
325
|
function handleRecordMatch(isMatch, newMatch) {
|
|
@@ -296,10 +337,10 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
296
337
|
|
|
297
338
|
if (maxMatchesFound({matches: matches.concat(newRecordMatches), maxMatches})) {
|
|
298
339
|
debug(`MaxMatches (${maxMatches}) reached, handled candidates in record set: ${newRecordCount} non-handled candidates in record set ${recordSetSize - newRecordCount}`);
|
|
299
|
-
return {matches: newRecordMatches, nonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: newRecordNonMatchCount};
|
|
340
|
+
return {matches: newRecordMatches, nonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: newRecordNonMatchCount, matchErrors: recordMatchErrors};
|
|
300
341
|
}
|
|
301
342
|
|
|
302
|
-
return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount});
|
|
343
|
+
return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount, matchErrors: recordMatchErrors});
|
|
303
344
|
}
|
|
304
345
|
|
|
305
346
|
function candidateNotInMatches(matches, candidate) {
|
|
@@ -40,7 +40,7 @@ export default ({nameTreshold = 10} = {}) => ({
|
|
|
40
40
|
|
|
41
41
|
function toObj({code, value}) {
|
|
42
42
|
if (code === 'a') {
|
|
43
|
-
return {name: value.replace(/[^\p{Letter}\p{Number}]/gu, '').toLowerCase()};
|
|
43
|
+
return {name: value ? value.replace(/[^\p{Letter}\p{Number}]/gu, '').toLowerCase() : ''};
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
return {id: value};
|
|
@@ -40,7 +40,8 @@ export default ({pattern, subfieldCodes}) => {
|
|
|
40
40
|
if (field) {
|
|
41
41
|
return field.subfields
|
|
42
42
|
.filter(({code}) => subfieldCodes.includes(code))
|
|
43
|
-
|
|
43
|
+
// .map(({code, value}) => ({code, value: value.replace(/-/ug, '')}));
|
|
44
|
+
.map(({code, value}) => ({code, value: value ? value.replace(/-/ug, '') : ''}));
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
return [];
|
|
@@ -32,7 +32,6 @@ import * as features from './features';
|
|
|
32
32
|
|
|
33
33
|
export {features};
|
|
34
34
|
|
|
35
|
-
// eslint-disable-next-line max-statements
|
|
36
35
|
export default ({strategy, treshold = 0.9}, returnStrategy = false) => (recordA, recordB) => {
|
|
37
36
|
const minProbabilityQuantifier = 0.5;
|
|
38
37
|
|
|
@@ -44,22 +43,36 @@ export default ({strategy, treshold = 0.9}, returnStrategy = false) => (recordA,
|
|
|
44
43
|
debugData(`ReturnStrategy: ${JSON.stringify(returnStrategy)}`);
|
|
45
44
|
|
|
46
45
|
const featuresA = extractFeatures(recordA);
|
|
47
|
-
const featuresB = extractFeatures(recordB);
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
debug(`We got an array of records: ${Array.isArray(recordB)}`);
|
|
48
|
+
const recordsB = Array.isArray(recordB) ? recordB : [recordB];
|
|
49
|
+
const detectionResults = recordsB.map(record => actualDetection(featuresA, record));
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
const similarityVector = generateSimilarityVector();
|
|
51
|
+
return Array.isArray(recordB) ? detectionResults : detectionResults[0];
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
function actualDetection(featuresA, record) {
|
|
54
|
+
|
|
55
|
+
const featuresB = extractFeatures(record);
|
|
56
|
+
|
|
57
|
+
debugData(`Features (a): ${JSON.stringify(featuresA)}`);
|
|
58
|
+
debugData(`Features (b): ${JSON.stringify(featuresB)}`);
|
|
59
|
+
|
|
60
|
+
const featurePairs = generateFeaturePairs(featuresA, featuresB);
|
|
61
|
+
const similarityVector = generateSimilarityVector(featurePairs);
|
|
62
|
+
|
|
63
|
+
if (similarityVector.some(v => v >= minProbabilityQuantifier)) {
|
|
64
|
+
const probability = calculateprobability(similarityVector);
|
|
65
|
+
debug(`probability: ${probability} (Treshold: ${treshold})`);
|
|
66
|
+
return returnResult({match: probability >= treshold, probability});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
debugData(`No feature yielded minimum probability amount of points (${minProbabilityQuantifier})`);
|
|
70
|
+
return returnResult({match: false, probability: 0.0});
|
|
59
71
|
}
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
function extractFeatures(record) {
|
|
74
|
+
return strategy.reduce((acc, {name, extract}) => acc.concat({name, value: extract(record)}), []);
|
|
75
|
+
}
|
|
63
76
|
|
|
64
77
|
function returnResult(result) {
|
|
65
78
|
if (returnStrategy) {
|
|
@@ -76,16 +89,12 @@ export default ({strategy, treshold = 0.9}, returnStrategy = false) => (recordA,
|
|
|
76
89
|
return strategyNames || [];
|
|
77
90
|
}
|
|
78
91
|
|
|
79
|
-
function calculateprobability() {
|
|
92
|
+
function calculateprobability(similarityVector) {
|
|
80
93
|
const probability = similarityVector.reduce((acc, v) => acc + v, 0.0);
|
|
81
94
|
return probability > 1.0 ? 1.0 : probability;
|
|
82
95
|
}
|
|
83
96
|
|
|
84
|
-
function
|
|
85
|
-
return strategy.reduce((acc, {name, extract}) => acc.concat({name, value: extract(record)}), []);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function generateSimilarityVector() {
|
|
97
|
+
function generateSimilarityVector(featurePairs) {
|
|
89
98
|
const compared = featurePairs.map(({name, a, b}) => {
|
|
90
99
|
const {compare} = strategy.find(({name: featureName}) => name === featureName);
|
|
91
100
|
const points = compare(a, b);
|
|
@@ -96,7 +105,7 @@ export default ({strategy, treshold = 0.9}, returnStrategy = false) => (recordA,
|
|
|
96
105
|
return compared.map(({points}) => points);
|
|
97
106
|
}
|
|
98
107
|
|
|
99
|
-
function generateFeaturePairs() {
|
|
108
|
+
function generateFeaturePairs(featuresA, featuresB) {
|
|
100
109
|
const pairs = generatePairs();
|
|
101
110
|
const missingFeatures = findMissing();
|
|
102
111
|
|
|
@@ -125,4 +134,5 @@ export default ({strategy, treshold = 0.9}, returnStrategy = false) => (recordA,
|
|
|
125
134
|
.filter(v => pairs.some(({name}) => name === v) === false);
|
|
126
135
|
}
|
|
127
136
|
}
|
|
137
|
+
|
|
128
138
|
};
|
|
@@ -32,6 +32,11 @@ import {expect} from 'chai';
|
|
|
32
32
|
import {MarcRecord} from '@natlibfi/marc-record';
|
|
33
33
|
import * as features from './features';
|
|
34
34
|
import createDetectionInterface from '.';
|
|
35
|
+
import {inspect} from 'util';
|
|
36
|
+
import createDebugLogger from 'debug';
|
|
37
|
+
|
|
38
|
+
const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:test');
|
|
39
|
+
const debugData = debug.extend('data');
|
|
35
40
|
|
|
36
41
|
describe('match-detection', () => {
|
|
37
42
|
generateTests({
|
|
@@ -41,16 +46,25 @@ describe('match-detection', () => {
|
|
|
41
46
|
fixura: {
|
|
42
47
|
reader: READERS.JSON
|
|
43
48
|
},
|
|
44
|
-
callback: ({getFixture, options, expectedResults, enabled = true}) => {
|
|
49
|
+
callback: ({getFixture, options, expectedResults, array, enabled = true}) => {
|
|
45
50
|
|
|
46
51
|
if (!enabled) {
|
|
52
|
+
debug(`*** DISABLED TEST! ***`);
|
|
47
53
|
return;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
const detect = createDetectionInterface(formatOptions());
|
|
51
|
-
const recordA = new MarcRecord(getFixture('recordA.json'));
|
|
52
|
-
|
|
57
|
+
const recordA = new MarcRecord(getFixture('recordA.json'), {subfieldValues: false});
|
|
58
|
+
debugData(inspect(recordA));
|
|
59
|
+
|
|
60
|
+
debug(`Our recordB is an array of records: ${array}`);
|
|
61
|
+
const recordB = array
|
|
62
|
+
? getFixture('recordB.json').map(recordJson => new MarcRecord(recordJson, {subfieldValues: false}))
|
|
63
|
+
: new MarcRecord(getFixture('recordB.json'), {subfieldValues: false});
|
|
64
|
+
debugData(inspect(recordB));
|
|
65
|
+
|
|
53
66
|
const results = detect(recordA, recordB);
|
|
67
|
+
debugData(`${JSON.stringify(results)}`);
|
|
54
68
|
|
|
55
69
|
expect(results).to.eql(expectedResults);
|
|
56
70
|
|
package/src/matching-utils.js
CHANGED
|
@@ -59,7 +59,7 @@ export function getMelindaIdsF035(record) {
|
|
|
59
59
|
return subfields
|
|
60
60
|
.filter(sub => ['a', 'z'].includes(sub.code))
|
|
61
61
|
.filter(sub => melindaIdRegExp.test(sub.value))
|
|
62
|
-
.map(({value}) => value.replace(melindaIdRegExp, '$<id>'));
|
|
62
|
+
.map(({value}) => value ? value.replace(melindaIdRegExp, '$<id>') : '');
|
|
63
63
|
|
|
64
64
|
}
|
|
65
65
|
}
|