@natlibfi/melinda-record-matching 2.0.1-alpha.1 → 2.0.7-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.
Files changed (96) hide show
  1. package/.github/CODEOWNERS +2 -0
  2. package/.github/dependabot.yml +40 -0
  3. package/.github/workflows/melinda-node-tests.yml +70 -0
  4. package/README.md +1 -1
  5. package/dist/candidate-search/candidate-search-utils.js +3 -6
  6. package/dist/candidate-search/candidate-search-utils.js.map +1 -1
  7. package/dist/candidate-search/index.js +115 -50
  8. package/dist/candidate-search/index.js.map +1 -1
  9. package/dist/candidate-search/index.spec.js +41 -48
  10. package/dist/candidate-search/index.spec.js.map +1 -1
  11. package/dist/candidate-search/query-list/bib.js +154 -64
  12. package/dist/candidate-search/query-list/bib.js.map +1 -1
  13. package/dist/candidate-search/query-list/bib.spec.js +1 -10
  14. package/dist/candidate-search/query-list/bib.spec.js.map +1 -1
  15. package/dist/candidate-search/query-list/index.js +4 -10
  16. package/dist/candidate-search/query-list/index.js.map +1 -1
  17. package/dist/index.js +498 -50
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.spec.js +60 -34
  20. package/dist/index.spec.js.map +1 -1
  21. package/dist/match-detection/features/bib/all-source-ids.js +16 -19
  22. package/dist/match-detection/features/bib/all-source-ids.js.map +1 -1
  23. package/dist/match-detection/features/bib/authors.js +31 -35
  24. package/dist/match-detection/features/bib/authors.js.map +1 -1
  25. package/dist/match-detection/features/bib/bibliographic-level.js +3 -3
  26. package/dist/match-detection/features/bib/bibliographic-level.js.map +1 -1
  27. package/dist/match-detection/features/bib/host-component.js +3 -3
  28. package/dist/match-detection/features/bib/host-component.js.map +1 -1
  29. package/dist/match-detection/features/bib/index.js +28 -13
  30. package/dist/match-detection/features/bib/index.js.map +1 -1
  31. package/dist/match-detection/features/bib/index.spec.js +9 -13
  32. package/dist/match-detection/features/bib/index.spec.js.map +1 -1
  33. package/dist/match-detection/features/bib/isbn.js +28 -6
  34. package/dist/match-detection/features/bib/isbn.js.map +1 -1
  35. package/dist/match-detection/features/bib/issn.js +0 -4
  36. package/dist/match-detection/features/bib/issn.js.map +1 -1
  37. package/dist/match-detection/features/bib/language.js +69 -41
  38. package/dist/match-detection/features/bib/language.js.map +1 -1
  39. package/dist/match-detection/features/bib/media-type.js +83 -0
  40. package/dist/match-detection/features/bib/media-type.js.map +1 -0
  41. package/dist/match-detection/features/bib/melinda-id.js +0 -4
  42. package/dist/match-detection/features/bib/melinda-id.js.map +1 -1
  43. package/dist/match-detection/features/bib/melinda-identifier-factory.js +6 -17
  44. package/dist/match-detection/features/bib/melinda-identifier-factory.js.map +1 -1
  45. package/dist/match-detection/features/bib/other-standard-identifier.js +1 -4
  46. package/dist/match-detection/features/bib/other-standard-identifier.js.map +1 -1
  47. package/dist/match-detection/features/bib/publication-time-allow-cons-years-multi.js +256 -0
  48. package/dist/match-detection/features/bib/publication-time-allow-cons-years-multi.js.map +1 -0
  49. package/dist/match-detection/features/bib/publication-time-allow-cons-years.js +71 -0
  50. package/dist/match-detection/features/bib/publication-time-allow-cons-years.js.map +1 -0
  51. package/dist/match-detection/features/bib/publication-time.js +9 -8
  52. package/dist/match-detection/features/bib/publication-time.js.map +1 -1
  53. package/dist/match-detection/features/bib/record-type.js +6 -3
  54. package/dist/match-detection/features/bib/record-type.js.map +1 -1
  55. package/dist/match-detection/features/bib/standard-identifier-factory.js +129 -39
  56. package/dist/match-detection/features/bib/standard-identifier-factory.js.map +1 -1
  57. package/dist/match-detection/features/bib/title-version-original.js +79 -0
  58. package/dist/match-detection/features/bib/title-version-original.js.map +1 -0
  59. package/dist/match-detection/features/bib/title.js +29 -22
  60. package/dist/match-detection/features/bib/title.js.map +1 -1
  61. package/dist/match-detection/features/index.js +0 -4
  62. package/dist/match-detection/features/index.js.map +1 -1
  63. package/dist/match-detection/index.js +94 -47
  64. package/dist/match-detection/index.js.map +1 -1
  65. package/dist/match-detection/index.spec.js +25 -15
  66. package/dist/match-detection/index.spec.js.map +1 -1
  67. package/dist/matching-utils.js +63 -15
  68. package/dist/matching-utils.js.map +1 -1
  69. package/package.json +27 -24
  70. package/src/candidate-search/index.js +93 -31
  71. package/src/candidate-search/index.spec.js +27 -24
  72. package/src/candidate-search/query-list/bib.js +149 -22
  73. package/src/candidate-search/query-list/index.js +1 -1
  74. package/src/index.js +310 -38
  75. package/src/index.spec.js +49 -21
  76. package/src/match-detection/features/bib/all-source-ids.js +3 -3
  77. package/src/match-detection/features/bib/authors.js +23 -16
  78. package/src/match-detection/features/bib/bibliographic-level.js +1 -1
  79. package/src/match-detection/features/bib/host-component.js +1 -1
  80. package/src/match-detection/features/bib/index.js +4 -0
  81. package/src/match-detection/features/bib/index.spec.js +10 -2
  82. package/src/match-detection/features/bib/isbn.js +21 -2
  83. package/src/match-detection/features/bib/language.js +63 -22
  84. package/src/match-detection/features/bib/media-type.js +77 -0
  85. package/src/match-detection/features/bib/melinda-identifier-factory.js +2 -3
  86. package/src/match-detection/features/bib/other-standard-identifier.js +2 -0
  87. package/src/match-detection/features/bib/publication-time-allow-cons-years-multi.js +268 -0
  88. package/src/match-detection/features/bib/publication-time-allow-cons-years.js +67 -0
  89. package/src/match-detection/features/bib/publication-time.js +9 -4
  90. package/src/match-detection/features/bib/record-type.js +5 -1
  91. package/src/match-detection/features/bib/standard-identifier-factory.js +107 -22
  92. package/src/match-detection/features/bib/title-version-original.js +78 -0
  93. package/src/match-detection/features/bib/title.js +27 -7
  94. package/src/match-detection/index.js +60 -22
  95. package/src/match-detection/index.spec.js +18 -4
  96. package/src/matching-utils.js +62 -5
package/dist/index.js CHANGED
@@ -4,30 +4,21 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.matchDetection = exports.default = exports.candidateSearch = void 0;
7
-
8
7
  var _debug = _interopRequireDefault(require("debug"));
9
-
10
8
  var candidateSearch = _interopRequireWildcard(require("./candidate-search"));
11
-
12
9
  exports.candidateSearch = candidateSearch;
13
-
14
10
  var matchDetection = _interopRequireWildcard(require("./match-detection"));
15
-
16
11
  exports.matchDetection = matchDetection;
17
-
18
12
  function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
19
-
20
13
  function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
21
-
22
14
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
23
-
24
15
  /**
25
16
  *
26
17
  * @licstart The following is the entire license notice for the JavaScript code in this file.
27
18
  *
28
19
  * Melinda record matching modules for Javascript
29
20
  *
30
- * Copyright (C) 2020 University Of Helsinki (The National Library Of Finland)
21
+ * Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)
31
22
  *
32
23
  * This file is part of melinda-record-matching-js
33
24
  *
@@ -48,82 +39,539 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
48
39
  * for the JavaScript code in this file.
49
40
  *
50
41
  */
42
+ //import inspect from 'util';
51
43
  var _default = ({
52
44
  detection: detectionOptions,
53
45
  search: searchOptions,
54
46
  maxMatches = 1,
55
- maxCandidates = 25
47
+ maxCandidates = 25,
48
+ returnStrategy = false,
49
+ returnQuery = false,
50
+ returnNonMatches = false,
51
+ returnFailures = false
56
52
  }) => {
57
53
  const debug = (0, _debug.default)('@natlibfi/melinda-record-matching:index');
58
- const detect = (0, matchDetection.default)(detectionOptions);
59
- return record => {
60
- const search = (0, candidateSearch.default)({ ...searchOptions,
61
- record
54
+ const debugData = debug.extend('data');
55
+ debugData(`DetectionOptions: ${JSON.stringify(detectionOptions)}`);
56
+ debugData(`SearchOptions: ${JSON.stringify(searchOptions)}`);
57
+ debugData(`MaxMatches: ${JSON.stringify(maxMatches)}`);
58
+ debugData(`MaxCandidates: ${JSON.stringify(maxCandidates)}`);
59
+ debugData(`ReturnStrategy: ${JSON.stringify(returnStrategy)}`);
60
+ debugData(`ReturnQuery: ${JSON.stringify(returnQuery)}`);
61
+ debugData(`ReturnNonMatches: ${JSON.stringify(returnNonMatches)}`);
62
+ debugData(`ReturnFailures: ${JSON.stringify(returnFailures)}`);
63
+ const detect = (0, matchDetection.default)(detectionOptions, returnStrategy);
64
+ return ({
65
+ record,
66
+ recordExternal = {
67
+ recordSource: 'incomingRecord',
68
+ label: 'ic'
69
+ }
70
+ }) => {
71
+ const search = (0, candidateSearch.default)({
72
+ ...searchOptions,
73
+ record,
74
+ maxCandidates,
75
+ recordExternal
62
76
  });
63
- return iterate(); // eslint-disable-next-line max-statements
77
+ return iterate({});
78
+
79
+ // candidateCount : amount of candidate records retrived from SRU for matching, NOT including current record set
80
+ // matches : candidates that have been detected as matches by current matcher job
81
+ // nonMatches : candidates that have been detected as non-matches by current matcher job (only if returnNonMatches is 'true')
82
+ // duplicateCount : amount of candidate records that were retrieved from the SRU but not handled further because they were already found in the matches/nonMatches
83
+
84
+ // state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)
85
+ // state.query : current query (undefined if there was no queries left)
86
+ // state.searchCounter : sequence for current search for current query (undefined, if there we no queries left)
87
+ // state.queryCandidateCounter: amount of candidate records retrieved from SRU for matching for current query, including the current record set (undefined if there were no queries left)
88
+ // state.queriesLeft : amount of queries left
89
+ // state.queryCounter : sequence for current query
90
+ // state.maxedQueries : queries that resulted in more than serverMaxResults hits
64
91
 
65
- async function iterate(initialState = {}, matches = [], candidateCount = 0) {
92
+ async function iterate({
93
+ initialState = {},
94
+ matches = [],
95
+ candidateCount = 0,
96
+ nonMatches = [],
97
+ duplicateCount = 0,
98
+ nonMatchCount = 0,
99
+ conversionFailures = [],
100
+ matchErrors = []
101
+ }) {
102
+ debugData(`Starting next matcher iteration.`);
66
103
  const {
67
104
  records,
105
+ failures,
68
106
  ...state
69
107
  } = await search(initialState);
108
+ debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, nonMatchCount: ${nonMatchCount}, conversionFailures: ${conversionFailures}, matchErrors: ${matchErrors.length}`);
109
+ const recordSetSize = records.length;
110
+ const failureSetSize = failures.length;
111
+ const newCandidateCount = candidateCount + recordSetSize + failureSetSize;
112
+ const newConversionFailures = conversionFailures.concat(failures);
113
+ debugData(`Failures: ${failures.length}, ConversionFailures: ${conversionFailures.length}, NewConversionFailures: ${newConversionFailures.length}`);
114
+ if (recordSetSize > 0) {
115
+ return handleRecordSet();
116
+ }
117
+ if (state.queriesLeft > 0) {
118
+ debug(`Empty record set ${state.searchCounter} for ${state.query}, but there are ${state.queriesLeft} queries left`);
119
+ return iterate({
120
+ initialState: state,
121
+ matches,
122
+ candidateCount: newCandidateCount,
123
+ nonMatches,
124
+ nonMatchCount,
125
+ duplicateCount,
126
+ conversionFailures: newConversionFailures,
127
+ matchErrors
128
+ });
129
+ }
130
+ debug(`No (more) candidate records to check, no more queries left, matches: ${matches.length}`);
131
+ return returnResult({
132
+ matches,
133
+ state,
134
+ stopReason: '',
135
+ nonMatches,
136
+ nonMatchCount,
137
+ candidateCount: newCandidateCount,
138
+ duplicateCount,
139
+ conversionFailures: newConversionFailures,
140
+ matchErrors
141
+ });
142
+ function handleRecordSet() {
143
+ debug(`Checking record set of ${recordSetSize} candidate records for possible matches, found by ${state.searchCounter} search for ${state.query}`);
144
+ const matchResult = iterateRecords({
145
+ records,
146
+ recordSetSize,
147
+ maxMatches,
148
+ matches,
149
+ nonMatches,
150
+ nonMatchCount
151
+ });
152
+ const newDuplicateCount = duplicateCount + matchResult.duplicateCount;
153
+ const newNonMatchCount = nonMatchCount + matchResult.nonMatchCount;
154
+ const {
155
+ newMatches,
156
+ newNonMatches,
157
+ newMatchErrors
158
+ } = handleMatchResult(matchResult, matches, nonMatches, matchErrors);
159
+ if (maxMatchesFound({
160
+ matches: newMatches,
161
+ maxMatches
162
+ })) {
163
+ return returnResult({
164
+ matches: newMatches,
165
+ state,
166
+ stopReason: 'maxMatches',
167
+ nonMatches: newNonMatches,
168
+ duplicateCount: newDuplicateCount,
169
+ candidateCount: newCandidateCount,
170
+ nonMatchCount: newNonMatchCount,
171
+ conversionFailures: newConversionFailures,
172
+ matchErrors: newMatchErrors
173
+ });
174
+ }
175
+ if (maxCandidatesRetrieved(newCandidateCount, maxCandidates)) {
176
+ return returnResult({
177
+ matches: newMatches,
178
+ state,
179
+ stopReason: 'maxCandidates',
180
+ nonMatches: newNonMatches,
181
+ duplicateCount: newDuplicateCount,
182
+ candidateCount: newCandidateCount,
183
+ nonMatchCount: newNonMatchCount,
184
+ conversionFailures: newConversionFailures,
185
+ matchErrors: newMatchErrors
186
+ });
187
+ }
188
+ return iterate({
189
+ initialState: state,
190
+ matches: newMatches,
191
+ candidateCount: newCandidateCount,
192
+ nonMatches: newNonMatches,
193
+ duplicateCount: newDuplicateCount,
194
+ nonMatchCount: newNonMatchCount,
195
+ conversionFailures: newConversionFailures,
196
+ matchErrors: newMatchErrors
197
+ });
198
+ }
199
+ function handleMatchResult(matchResult, matches, nonMatches, matchErrors) {
200
+ debugData(`- Amount of new matches from record set: ${matchResult.matches.length}`);
201
+ // eslint-disable-next-line functional/no-conditional-statements
202
+ if (returnNonMatches) {
203
+ debugData(`- Amount of new nonMatches from record set: ${matchResult.nonMatches.length}`);
204
+ }
205
+ const newMatches = matches.concat(returnQuery ? addQuery(matchResult.matches) : matchResult.matches);
206
+ const newNonMatches = returnNonMatches ? nonMatches.concat(returnQuery ? addQuery(matchResult.nonMatches) : matchResult.nonMatches) : [];
207
+ const newMatchErrors = matchErrors.concat(matchResult.matchErrors);
208
+ debugData(`- Total amount of matches: ${newMatches.length}`);
209
+ // eslint-disable-next-line functional/no-conditional-statements
210
+ if (returnNonMatches) {
211
+ debugData(`- Total amount of nonMatches: ${newNonMatches.length}`);
212
+ }
213
+ debugData(`MatchResult: ${JSON.stringify(matchResult)}`);
214
+ debugData(`Old matchErrors: ${JSON.stringify(matchErrors)}, matchErrors from matchResult: ${JSON.stringify(matchResult.matchErrors)}, New matchErrors: ${JSON.stringify(newMatchErrors)}`);
215
+ debugData(`- Total amount of matchErrors: ${newMatchErrors.length}`);
216
+ return {
217
+ newMatches,
218
+ newNonMatches,
219
+ newMatchErrors
220
+ };
221
+ }
222
+ function addQuery(matches) {
223
+ debugData(`Adding query ${state.query} to matches`);
224
+ return matches.map(match => ({
225
+ ...match,
226
+ matchQuery: state.query
227
+ }));
228
+ }
229
+ function maxCandidatesRetrieved(candidateCount, maxCandidates) {
230
+ debugData(`Total amount of candidate records retrieved: ${newCandidateCount} (max: ${maxCandidates})`);
231
+ if (maxCandidates && candidateCount >= maxCandidates) {
232
+ debug(`Stopped matching because maximum number of candidate records ${candidateCount} / ${maxCandidates} have been retrieved`);
233
+ return true;
234
+ }
235
+ }
236
+ }
70
237
 
71
- if (records.length > 0 || state.queriesLeft > 0) {
72
- debug(`Checking ${records.length} candidates for matches`);
73
- const matchResult = iterateRecords(records);
238
+ // matches : array of matching candidate records
239
+ // nonMatches : array of nonMatching candidate records (if returnNonMatches option is true, otherwise empty array)
240
+ // - candidate.id
241
+ // - candidate.record
242
+ // - probability
243
+ // - strategy (if returnStrategy option is true)
244
+ // - treshold (if returnStrategy option is true)
245
+ // - matchQuery (if returnQuery option is true)
246
+ // failures: array of conversionFailures from candidate-search and matchErrors from matchDetection in error format {status, payload: {message, id}} if returnFailures is true
74
247
 
75
- if (matchResult) {
76
- const newMatches = matches.concat(matchResult);
248
+ // we could have here also returnRecords/returnMatchRecords/returnNonMatchRecord options that could be turned false for not to return actual record data
77
249
 
78
- if (newMatches.length === maxMatches) {
79
- return newMatches;
80
- }
250
+ // matchStatus.status: boolean, true if matcher retrieved and handled all found candidate records, false if it did not
251
+ // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries','conversionFailures', empty string/undefined), reason for stopping retrieving or handling the candidate records
252
+ // - only one stopReason is returned (if there would be several possible stopReasons, stopReason is picked in the above order)
253
+ // - 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
81
254
 
82
- return maxCandidatesRetrieved() ? newMatches : iterate(state, newMatches, candidateCount + records.length);
83
- }
255
+ function returnResult({
256
+ matches,
257
+ state,
258
+ stopReason,
259
+ nonMatches,
260
+ duplicateCount,
261
+ candidateCount,
262
+ nonMatchCount,
263
+ conversionFailures,
264
+ matchErrors
265
+ }) {
266
+ const conversionFailureCount = conversionFailures.length;
267
+ const matchErrorCount = matchErrors.length;
268
+ checkCounts({
269
+ matches,
270
+ nonMatches,
271
+ candidateCount,
272
+ duplicateCount,
273
+ nonMatchCount,
274
+ conversionFailureCount,
275
+ matchErrorCount
276
+ });
277
+ const matchStatus = getMatchState(state, stopReason, conversionFailureCount, matchErrorCount);
278
+ // add nonMatches to result only if returnNonMatches is 'true', otherwise nonMatches have not been gathered
279
+ const matchesResult = returnNonMatches ? {
280
+ matches,
281
+ matchStatus,
282
+ nonMatches,
283
+ candidateCount
284
+ } : {
285
+ matches,
286
+ matchStatus,
287
+ candidateCount
288
+ };
289
+ const failures = [...conversionFailures, ...matchErrors];
290
+ const result = returnFailures ? {
291
+ ...matchesResult,
292
+ conversionFailures: failures
293
+ } : matchesResult;
294
+ debugData(`ReturnFailures ${returnFailures}`);
295
+ debugData(`${JSON.stringify(result)}`);
296
+ return result;
84
297
 
85
- return maxCandidatesRetrieved() ? matches : iterate(state, matches, candidateCount + records.length);
298
+ // note that in cases where the matching has been stopped because of maxMatches checkCounts won't (in most cases) match
299
+
300
+ function checkCounts({
301
+ matches,
302
+ nonMatches,
303
+ candidateCount,
304
+ duplicateCount,
305
+ nonMatchCount,
306
+ conversionFailureCount,
307
+ matchErrorCount
308
+ }) {
309
+ const matchCount = matches.length;
310
+ debugData(`Return nonMatches: ${returnNonMatches}`);
311
+ const chosenNonMatchCount = returnNonMatches ? nonMatches.length : nonMatchCount;
312
+ const totalHandled = matchCount + chosenNonMatchCount + duplicateCount;
313
+ debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}, conversionFailureCount: ${conversionFailureCount}, matchErrorCount: ${matchErrorCount}`);
314
+ debug(`We got result for ${totalHandled} / ${candidateCount} retrieved candidates`);
315
+ if (totalHandled !== candidateCount) {
316
+ debug(`WARNING: Missing results for ${candidateCount - totalHandled} candidates`);
317
+ return;
318
+ }
319
+ return;
86
320
  }
87
321
 
88
- debug(`No (more) candidate records to check, matches: ${matches.length}`);
89
- return matches;
322
+ // eslint-disable-next-line max-statements
323
+ function getMatchState(state, stopReason, conversionFailuresCount, matchErrorCount) {
324
+ debugData(`${JSON.stringify(state)}`);
325
+ debug(`We had ${conversionFailuresCount} retrieved candidates that could not be converted.`);
326
+ debug(`We had ${matchErrorCount} retrieved candidates that errored in matchDetection.`);
327
+ 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}`);
328
+ debugData(`StopReason: <${stopReason}>`);
329
+ const searchesLeft = state.resultSetOffset && state.resultSetOffset <= state.totalRecords;
330
+ const nonRetrieved = searchesLeft ? state.totalRecords - state.queryCandidateCounter : 0;
331
+ debugData(`nonRetrieved: ${nonRetrieved}`);
90
332
 
91
- function maxCandidatesRetrieved() {
92
- if (candidateCount + records.length > maxCandidates) {
93
- debug(`Stopped searching because maximum number of candidates have been retrieved`);
94
- return true;
333
+ // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries','conversionFailures', empty string/undefined), reason for stopping retrieving or handling the candidate records
334
+ // 'maxMatches' and 'maxCandidates' are in stopReason, 'maxedQueries', 'conversionFailures' and 'matchErrors' are created here
335
+
336
+ if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0 || conversionFailureCount > 0 || matchErrorCount > 0) {
337
+ const maxedQueriesStopReason = state.maxedQueries.length > 0 ? 'maxedQueries' : undefined;
338
+ const conversionFailuresStopReason = conversionFailureCount > 0 ? 'conversionFailures' : undefined;
339
+ const matchErrorsStopReason = matchErrorCount > 0 ? 'matchErrors' : undefined;
340
+ const newStopReason = stopReason === '' || stopReason === undefined ? maxedQueriesStopReason || conversionFailuresStopReason || matchErrorsStopReason : stopReason;
341
+ debugData(`MaxedQueriesStopReason: <${maxedQueriesStopReason}>`);
342
+ debugData(`ConversionFailureStopReason <${conversionFailuresStopReason}>`);
343
+ debugData(`MatchErrorsStopReason <${matchErrorsStopReason}>`);
344
+ debugData(`NewStopReason: <${newStopReason}>`);
345
+ debug(`Match status: false`);
346
+ return {
347
+ status: false,
348
+ stopReason: newStopReason
349
+ };
95
350
  }
351
+ debug(`Match status: true`);
352
+ return {
353
+ status: true,
354
+ stopReason
355
+ };
96
356
  }
357
+ }
358
+
359
+ // NOTES:
360
+ // - we could optimize by creating the featureSet for the incoming record once and using it for all database/candidateRecords
361
+ // - if creating the featureSet for the incoming record fails we have an unprocessable entity
362
+ // - if creating the featureSet for a candidate record fails we could skip that candidate - but list the case as a detectionFailure, same as conversionFailures
363
+
364
+ function iterateRecords({
365
+ records,
366
+ recordSetSize,
367
+ maxMatches,
368
+ matches = [],
369
+ nonMatches = [],
370
+ recordMatches = [],
371
+ recordNonMatches = [],
372
+ recordCount = 0,
373
+ recordDuplicateCount = 0,
374
+ recordNonMatchCount = 0,
375
+ recordMatchErrors = []
376
+ }) {
377
+ // recordSetSize : total amount of records in the current record set
378
+ // recordCount : amount of records from the current record set that have been handled
379
+ // maxMatches : setting for maximum amount found by current matcher job before the matcher job is stopped
380
+ // recordDuplicateCount : amount of records from the current record set that are already included in matches/nonMatches results
381
+ // recordNonMatchCount: amount of records from the current record set that are nonMatches (only is returnNonMatches setting is false)
382
+
383
+ // records : non-handled records in the current record set
384
+ // matches : found matches in the current matcher job
385
+ // recordMatches : found matches in the current record set
386
+ // recordNonMatches : found nonMatches in the current record set (only if returnNonMatches setting is true)
387
+ // recordMatchErrors: errored matchDetection in the current record set
388
+
389
+ const [candidate] = records;
390
+ const newRecordCount = candidate ? recordCount + 1 : recordCount;
97
391
 
98
- function iterateRecords(records) {
99
- const [candidate] = records;
392
+ // The matcher uses same matchDetection strategy for candidates from all candidate-searches -> matchDetection result for the same candidate is always same
393
+ // Exceptions would happen if the candidate would have been updated in the database between candidate searches
394
+ // 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
395
+ // different candidate search queries. Same candidate search query won't have duplicate records.
100
396
 
101
- if (candidate) {
397
+ /* We could optimize and detect all retrieved candidates at once
398
+ const candidateRecords = records.map(record => record.record);
399
+ const recordsIsArray = Array.isArray(candidateRecords);
400
+ debug(`records is an array: ${recordsIsArray}`);
401
+ const result = detect(record, candidateRecords);
402
+ debugData(`${JSON.stringify(result)}`);
403
+ */
404
+
405
+ if (candidate) {
406
+ // eslint-disable-next-line functional/no-conditional-statements
407
+ if (candidateNotInMatches(matches.concat(nonMatches), candidate)) {
102
408
  const {
103
409
  record: candidateRecord,
104
410
  id: candidateId
105
411
  } = candidate;
106
- const {
107
- match,
108
- probability
109
- } = detect(record, candidateRecord);
110
-
111
- if (match) {
112
- return {
113
- probability,
114
- candidate: {
115
- id: candidateId,
116
- record: candidateRecord
412
+ const recordBExternal = {
413
+ id: candidate.id,
414
+ recordSource: 'databaseRecord',
415
+ label: `db-${candidate.id}`
416
+ };
417
+ try {
418
+ debug(`Running matchDetection for record ${candidateId} (${newRecordCount}/${recordSetSize})`);
419
+ // we should handle errors from detection somehow - ie. cases where either record or candidateRecord errors
420
+ const detectionResult = detect({
421
+ recordA: record,
422
+ recordB: candidateRecord,
423
+ recordAExternal: recordExternal,
424
+ recordBExternal
425
+ });
426
+ return handleDetectionResult(detectionResult, candidateId, candidateRecord);
427
+ } catch (error) {
428
+ debug(`MatchDetection errored: database record ${candidateId}: ${error}`);
429
+ const matchError = {
430
+ status: 422,
431
+ payload: {
432
+ message: `Matching errored for database record ${candidateId}. ${error.message}.`,
433
+ id: candidateId
117
434
  }
118
435
  };
436
+ const newRecordMatchErrors = recordMatchErrors.concat(matchError);
437
+ return iterateRecords({
438
+ records: records.slice(1),
439
+ recordSetSize,
440
+ maxMatches,
441
+ matches,
442
+ recordMatches,
443
+ recordCount: newRecordCount,
444
+ recordNonMatches,
445
+ recordDuplicateCount,
446
+ recordNonMatchCount,
447
+ recordMatchErrors: newRecordMatchErrors
448
+ });
119
449
  }
450
+ }
451
+ return iterateRecords({
452
+ records: records.slice(1),
453
+ recordSetSize,
454
+ maxMatches,
455
+ matches,
456
+ recordMatches,
457
+ recordCount: newRecordCount,
458
+ recordNonMatches,
459
+ recordDuplicateCount: recordDuplicateCount + 1,
460
+ recordNonMatchCount,
461
+ recordMatchErrors
462
+ });
463
+ }
464
+ debug(`No more candidates, record set (${recordCount}/${recordSetSize}) done, ${recordMatches.length} matches found, ${recordDuplicateCount} candidates already handled, ${returnNonMatches ? `${recordNonMatches.length}` : `${recordNonMatchCount}`} nonMatches found.`);
465
+ return {
466
+ matches: recordMatches,
467
+ nonMatches: returnNonMatches ? recordNonMatches : [],
468
+ duplicateCount: recordDuplicateCount,
469
+ nonMatchCount: recordNonMatchCount,
470
+ matchErrors: recordMatchErrors
471
+ };
472
+ function handleDetectionResult(detectionResult, candidateId, candidateRecord) {
473
+ debugData(`MatchDetection results for ${candidateId} (${newRecordCount}/${recordSetSize}): ${JSON.stringify(detectionResult)}`);
474
+ if (detectionResult.match || returnNonMatches) {
475
+ debug(`${detectionResult.match ? `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is a match!` : `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is NOT a match!`}`);
476
+ debugData(`Strategy: ${JSON.stringify(detectionResult.strategy)}, Treshold: ${JSON.stringify(detectionResult.treshold)}`);
477
+ const matchResult = {
478
+ probability: detectionResult.probability,
479
+ candidate: {
480
+ id: candidateId,
481
+ record: candidateRecord
482
+ }
483
+ };
484
+ const strategyResult = {
485
+ strategy: detectionResult.strategy,
486
+ treshold: detectionResult.treshold
487
+ };
488
+ const newMatch = returnStrategy ? {
489
+ ...matchResult,
490
+ ...strategyResult
491
+ } : {
492
+ ...matchResult
493
+ };
494
+ debugData(`${JSON.stringify(newMatch)}`);
495
+ return handleRecordMatch(detectionResult.match, newMatch);
496
+ }
497
+ const newRecordNonMatchCount = recordNonMatchCount + 1;
498
+ debugData(`- Total nonMatches after this detection: ${newRecordNonMatchCount}`);
499
+ return iterateRecords({
500
+ records: records.slice(1),
501
+ recordSetSize,
502
+ maxMatches,
503
+ matches,
504
+ recordMatches,
505
+ recordCount: newRecordCount,
506
+ recordNonMatches,
507
+ recordDuplicateCount,
508
+ recordNonMatchCount: newRecordNonMatchCount,
509
+ recordMatchErrors
510
+ });
511
+ }
512
+ function handleRecordMatch(isMatch, newMatch) {
513
+ const newRecordMatches = isMatch ? recordMatches.concat(newMatch) : recordMatches;
514
+ const newRecordNonMatches = isMatch ? recordNonMatches : recordNonMatches.concat(newMatch);
515
+ const newRecordNonMatchCount = isMatch ? recordNonMatchCount : recordNonMatchCount + 1;
516
+ debugData(`- Total matches after this detection: ${matches.concat(newRecordMatches).length} (max: ${maxMatches})`);
120
517
 
121
- return iterateRecords(records.slice(1));
518
+ // eslint-disable-next-line functional/no-conditional-statements
519
+ if (returnNonMatches) {
520
+ debugData(`- Total nonMatches after this detection: ${nonMatches.concat(newRecordNonMatches).length}`);
122
521
  }
522
+ debugData(`- Total nonMatchCount after this detection: ${recordNonMatchCount}`);
523
+ if (maxMatchesFound({
524
+ matches: matches.concat(newRecordMatches),
525
+ maxMatches
526
+ })) {
527
+ debug(`MaxMatches (${maxMatches}) reached, handled candidates in record set: ${newRecordCount} non-handled candidates in record set ${recordSetSize - newRecordCount}`);
528
+ return {
529
+ matches: newRecordMatches,
530
+ nonMatches: returnNonMatches ? newRecordNonMatches : [],
531
+ duplicateCount: recordDuplicateCount,
532
+ nonMatchCount: newRecordNonMatchCount,
533
+ matchErrors: recordMatchErrors
534
+ };
535
+ }
536
+ return iterateRecords({
537
+ records: records.slice(1),
538
+ recordSetSize,
539
+ maxMatches,
540
+ matches,
541
+ recordMatches: newRecordMatches,
542
+ recordCount: newRecordCount,
543
+ recordNonMatches: returnNonMatches ? newRecordNonMatches : [],
544
+ duplicateCount: recordDuplicateCount,
545
+ recordNonMatchCount: newRecordNonMatchCount,
546
+ matchErrors: recordMatchErrors
547
+ });
548
+ }
549
+ function candidateNotInMatches(matches, candidate) {
550
+ debug(`Checking that record ${candidate.id} is not already included in ${matches.length} matches/nonMatches`);
551
+ const newCandidateId = candidate.id;
552
+ debugData(`newCandidateId: ${newCandidateId}`);
553
+ const result = matches.find(({
554
+ candidate
555
+ }) => candidate.id === newCandidateId);
556
+ debugData(`Result: ${result}`);
557
+ if (result) {
558
+ debug(`${candidate.id} was already handled.`);
559
+ return false;
560
+ }
561
+ debug(`${candidate.id} not found in matches/nonMatches`);
562
+ return true;
563
+ }
564
+ }
565
+ function maxMatchesFound({
566
+ matches,
567
+ maxMatches
568
+ }) {
569
+ if (maxMatches && matches.length >= maxMatches) {
570
+ debug(`Stopping recordSet iteration: maxMatches (${maxMatches}) for matcher job found.`);
571
+ return true;
123
572
  }
124
573
  }
125
574
  };
126
575
  };
127
-
128
576
  exports.default = _default;
129
577
  //# sourceMappingURL=index.js.map