@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.
Files changed (45) hide show
  1. package/dist/candidate-search/candidate-search-utils.js.map +1 -1
  2. package/dist/candidate-search/index.js +9 -9
  3. package/dist/candidate-search/index.js.map +1 -1
  4. package/dist/candidate-search/index.spec.js.map +1 -1
  5. package/dist/candidate-search/query-list/bib.js +2 -2
  6. package/dist/candidate-search/query-list/bib.js.map +1 -1
  7. package/dist/candidate-search/query-list/bib.spec.js.map +1 -1
  8. package/dist/candidate-search/query-list/index.js.map +1 -1
  9. package/dist/index.js +103 -33
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.spec.js.map +1 -1
  12. package/dist/match-detection/features/bib/all-source-ids.js.map +1 -1
  13. package/dist/match-detection/features/bib/authors.js +1 -1
  14. package/dist/match-detection/features/bib/authors.js.map +1 -1
  15. package/dist/match-detection/features/bib/bibliographic-level.js.map +1 -1
  16. package/dist/match-detection/features/bib/host-component.js.map +1 -1
  17. package/dist/match-detection/features/bib/index.js.map +1 -1
  18. package/dist/match-detection/features/bib/index.spec.js.map +1 -1
  19. package/dist/match-detection/features/bib/isbn.js.map +1 -1
  20. package/dist/match-detection/features/bib/issn.js.map +1 -1
  21. package/dist/match-detection/features/bib/language.js.map +1 -1
  22. package/dist/match-detection/features/bib/melinda-id.js.map +1 -1
  23. package/dist/match-detection/features/bib/melinda-identifier-factory.js.map +1 -1
  24. package/dist/match-detection/features/bib/other-standard-identifier.js.map +1 -1
  25. package/dist/match-detection/features/bib/publication-time.js.map +1 -1
  26. package/dist/match-detection/features/bib/record-type.js.map +1 -1
  27. package/dist/match-detection/features/bib/standard-identifier-factory.js +3 -2
  28. package/dist/match-detection/features/bib/standard-identifier-factory.js.map +1 -1
  29. package/dist/match-detection/features/bib/title.js.map +1 -1
  30. package/dist/match-detection/features/index.js.map +1 -1
  31. package/dist/match-detection/index.js +36 -30
  32. package/dist/match-detection/index.js.map +1 -1
  33. package/dist/match-detection/index.spec.js +20 -2
  34. package/dist/match-detection/index.spec.js.map +1 -1
  35. package/dist/matching-utils.js +1 -1
  36. package/dist/matching-utils.js.map +1 -1
  37. package/package.json +13 -13
  38. package/src/candidate-search/index.js +11 -8
  39. package/src/candidate-search/query-list/bib.js +2 -2
  40. package/src/index.js +71 -30
  41. package/src/match-detection/features/bib/authors.js +1 -1
  42. package/src/match-detection/features/bib/standard-identifier-factory.js +2 -1
  43. package/src/match-detection/index.js +29 -19
  44. package/src/match-detection/index.spec.js +17 -3
  45. package/src/matching-utils.js +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/matching-utils.js"],"names":["debug","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"],"mappings":"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","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.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"],"file":"matching-utils.js"}
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.1",
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.2",
44
- "natural": "^5.1.13",
43
+ "moment": "^2.29.4",
44
+ "natural": "^5.2.3",
45
45
  "uuid": "^8.3.2",
46
- "winston": "^3.7.2"
46
+ "winston": "^3.8.1"
47
47
  },
48
48
  "devDependencies": {
49
- "@babel/cli": "^7.17.6",
50
- "@babel/core": "^7.17.9",
51
- "@babel/eslint-parser": "^7.17.0",
52
- "@babel/node": "^7.16.8",
53
- "@babel/preset-env": "^7.16.11",
54
- "@babel/register": "^7.17.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.2",
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.13.0",
64
+ "eslint": "^8.23.0",
65
65
  "mocha": "^9.2.2",
66
- "nodemon": "^2.0.15",
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', foundRecordXML => {
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 foundRecordMarc = await MARCXML.from(foundRecordXML, {subfieldValues: false});
158
- const foundRecordId = getRecordId(foundRecordMarc);
160
+ const recordMarc = await MARCXML.from(recordXML, {subfieldValues: false});
161
+ const recordId = getRecordId(recordMarc);
159
162
 
160
- return {record: foundRecordMarc, id: foundRecordId};
163
+ return {record: recordMarc, id: recordId};
161
164
  } catch (err) {
162
165
  // What should this do?
163
- const idFromXML = getRecordIdFromXML(foundRecordXML);
164
- debugData(`Failed converting record: ${err.message}, id: ${idFromXML}, data: ${foundRecordXML}`);
165
- //return {message: `Failed converting record: ${err.message}`, id: idFromXML, data: foundRecordXML};
166
- throw new MatchingError(422, {message: `Failed converting record: ${err.message}`, id: idFromXML || '000000000', data: foundRecordXML});
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
- return {newMatches, newNonMatches};
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
- checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, conversionFailureCount});
164
- const matchStatus = getMatchState(state, stopReason, conversionFailureCount);
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 result = returnFailures ? {...matchesResult, conversionFailures} : matchesResult;
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
- function getMatchState(state, stopReason, conversionFailuresCount) {
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 'conversionFailures' are created here
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 newStopReason = stopReason === '' || stopReason === undefined ? maxedQueriesStopReason || conversionFailuresStopReason : stopReason;
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
- function iterateRecords({records, recordSetSize, maxMatches, matches = [], nonMatches = [], recordMatches = [], recordNonMatches = [], recordCount = 0, recordDuplicateCount = 0, recordNonMatchCount = 0}) {
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
- debug(`Running matchDetection for record ${candidateId} (${newRecordCount}/${recordSetSize})`);
244
- const detectionResult = detect(record, candidateRecord);
245
- return handleDetectionResult(detectionResult, candidateId, candidateRecord);
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
- return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount: recordDuplicateCount + 1, recordNonMatchCount});
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
- .map(({code, value}) => ({code, value: value.replace(/-/ug, '')}));
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
- debugData(`Features (a): ${JSON.stringify(featuresA)}`);
50
- debugData(`Features (b): ${JSON.stringify(featuresB)}`);
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
- const featurePairs = generateFeaturePairs();
53
- const similarityVector = generateSimilarityVector();
51
+ return Array.isArray(recordB) ? detectionResults : detectionResults[0];
54
52
 
55
- if (similarityVector.some(v => v >= minProbabilityQuantifier)) {
56
- const probability = calculateprobability();
57
- debug(`probability: ${probability} (Treshold: ${treshold})`);
58
- return returnResult({match: probability >= treshold, probability});
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
- debugData(`No feature yielded minimum probability amount of points (${minProbabilityQuantifier})`);
62
- return returnResult({match: false, probability: 0.0});
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 extractFeatures(record) {
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
- const recordB = new MarcRecord(getFixture('recordB.json'));
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
 
@@ -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
  }