@natlibfi/melinda-record-matching 2.0.1-alpha.1 → 2.1.0

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/src/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Melinda record matching modules for Javascript
6
6
  *
7
- * Copyright (C) 2020 University Of Helsinki (The National Library Of Finland)
7
+ * Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)
8
8
  *
9
9
  * This file is part of melinda-record-matching-js
10
10
  *
@@ -32,65 +32,277 @@ import createDetectionInterface, * as matchDetection from './match-detection';
32
32
 
33
33
  export {candidateSearch, matchDetection};
34
34
 
35
- export default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25}) => {
35
+ export default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false}) => {
36
36
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:index');
37
- const detect = createDetectionInterface(detectionOptions);
37
+ const debugData = debug.extend('data');
38
+
39
+ debugData(`DetectionOptions: ${JSON.stringify(detectionOptions)}`);
40
+ debugData(`SearchOptions: ${JSON.stringify(searchOptions)}`);
41
+ debugData(`MaxMatches: ${JSON.stringify(maxMatches)}`);
42
+ debugData(`MaxCandidates: ${JSON.stringify(maxCandidates)}`);
43
+ debugData(`ReturnStrategy: ${JSON.stringify(returnStrategy)}`);
44
+ debugData(`ReturnQuery: ${JSON.stringify(returnQuery)}`);
45
+ debugData(`ReturnNonMatches: ${JSON.stringify(returnNonMatches)}`);
46
+
47
+ const detect = createDetectionInterface(detectionOptions, returnStrategy);
38
48
 
39
49
  return record => {
40
- const search = createSearchInterface({...searchOptions, record});
41
- return iterate();
50
+ const search = createSearchInterface({...searchOptions, record, maxCandidates});
51
+ return iterate({});
52
+
53
+ // candidateCount : amount of candidate records retrived from SRU for matching, NOT including current record set
54
+ // matches : candidates that have been detected as matches by current matcher job
55
+ // nonMatches : candidates that have been detected as non-matches by current matcher job (only if returnNonMatches is 'true')
56
+ // duplicateCount : amount of candidate records that were retrieved from the SRU but not handled further because they were already found in the matches/nonMatches
57
+
58
+ // state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)
59
+ // state.query : current query (undefined if there was no queries left)
60
+ // state.searchCounter : sequence for current search for current query (undefined, if there we no queries left)
61
+ // 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)
62
+ // state.queriesLeft : amount of queries left
63
+ // state.queryCounter : sequence for current query
64
+ // state.maxedQueries : queries that resulted in more than serverMaxResults hits
42
65
 
43
- // eslint-disable-next-line max-statements
44
- async function iterate(initialState = {}, matches = [], candidateCount = 0) {
66
+ async function iterate({initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0, nonMatchCount = 0}) {
67
+ debugData(`Starting next matcher iteration.`);
45
68
  const {records, ...state} = await search(initialState);
46
69
 
47
- if (records.length > 0 || state.queriesLeft > 0) {
48
- debug(`Checking ${records.length} candidates for matches`);
70
+ debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}`);
71
+ const recordSetSize = records.length;
72
+ const newCandidateCount = candidateCount + recordSetSize;
73
+
74
+ if (recordSetSize > 0) {
75
+ return handleRecordSet();
76
+ }
77
+
78
+ if (state.queriesLeft > 0) {
79
+ debug(`Empty record set ${state.searchCounter} for ${state.query}, but there are ${state.queriesLeft} queries left`);
80
+ return iterate({initialState: state, matches, candidateCount: newCandidateCount, nonMatches, duplicateCount});
81
+ }
82
+
83
+ debug(`No (more) candidate records to check, no more queries left, matches: ${matches.length}`);
84
+ return returnResult({matches, state, stopReason: '', nonMatches, candidateCount: newCandidateCount, duplicateCount});
85
+
86
+ function handleRecordSet() {
87
+ debug(`Checking record set of ${recordSetSize} candidate records for possible matches, found by ${state.searchCounter} search for ${state.query}`);
88
+
89
+ const matchResult = iterateRecords({records, recordSetSize, maxMatches, matches, nonMatches});
90
+ const newDuplicateCount = duplicateCount + matchResult.duplicateCount;
91
+ const newNonMatchCount = nonMatchCount + matchResult.nonMatchCount;
92
+ const {newMatches, newNonMatches} = handleMatchResult(matchResult, matches, nonMatches);
93
+
94
+ if (maxMatchesFound({matches: newMatches, maxMatches})) {
95
+ return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount});
96
+ }
97
+
98
+ if (maxCandidatesRetrieved(newCandidateCount, maxCandidates)) {
99
+ return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount});
100
+ }
49
101
 
50
- const matchResult = iterateRecords(records);
102
+ return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount});
103
+ }
51
104
 
52
- if (matchResult) {
53
- const newMatches = matches.concat(matchResult);
105
+ function handleMatchResult(matchResult, matches, nonMatches) {
106
+ debugData(`- Amount of new matches from record set: ${matchResult.matches.length}`);
107
+ // eslint-disable-next-line functional/no-conditional-statement
108
+ if (returnNonMatches) {
109
+ debugData(`- Amount of new nonMatches from record set: ${matchResult.nonMatches.length}`);
110
+ }
54
111
 
55
- if (newMatches.length === maxMatches) {
56
- return newMatches;
57
- }
112
+ const newMatches = matches.concat(returnQuery ? addQuery(matchResult.matches) : matchResult.matches);
113
+ const newNonMatches = returnNonMatches ? nonMatches.concat(returnQuery ? addQuery(matchResult.nonMatches) : matchResult.nonMatches) : [];
58
114
 
59
- return maxCandidatesRetrieved() ? newMatches : iterate(state, newMatches, candidateCount + records.length);
115
+ debugData(`- Total amount of matches: ${newMatches.length}`);
116
+ // eslint-disable-next-line functional/no-conditional-statement
117
+ if (returnNonMatches) {
118
+ debugData(`- Total amount of nonMatches: ${newNonMatches.length}`);
60
119
  }
61
120
 
62
- return maxCandidatesRetrieved() ? matches : iterate(state, matches, candidateCount + records.length);
121
+ return {newMatches, newNonMatches};
63
122
  }
64
123
 
65
- debug(`No (more) candidate records to check, matches: ${matches.length}`);
66
- return matches;
124
+ function addQuery(matches) {
125
+ debugData(`Adding query ${state.query} to matches`);
126
+ return matches.map((match) => ({...match, matchQuery: state.query}));
127
+ }
67
128
 
68
- function maxCandidatesRetrieved() {
69
- if (candidateCount + records.length > maxCandidates) {
70
- debug(`Stopped searching because maximum number of candidates have been retrieved`);
129
+ function maxCandidatesRetrieved(candidateCount, maxCandidates) {
130
+ debugData(`Total amount of candidate records retrieved: ${newCandidateCount} (max: ${maxCandidates})`);
131
+ if (maxCandidates && candidateCount >= maxCandidates) {
132
+ debug(`Stopped matching because maximum number of candidate records ${candidateCount} / ${maxCandidates} have been retrieved`);
71
133
  return true;
72
134
  }
73
135
  }
136
+ }
137
+
138
+ // matches : array of matching candidate records
139
+ // nonMatches : array of nonMatching candidate records (if returnNonMatches option is true, otherwise empty array)
140
+ // - candidate.id
141
+ // - candidate.record
142
+ // - probability
143
+ // - strategy (if returnStrategy option is true)
144
+ // - treshold (if returnStrategy option is true)
145
+ // - matchQuery (if returnQuery option is true)
146
+
147
+ // we could have here also returnRecords/returnMatchRecords/returnNonMatchRecord options that could be turned false for not to return actual record data
148
+
149
+ // matchStatus.status: boolean, true if matcher retrieved and handled all found candidate records, false if it did not
150
+ // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries',empty string/undefined), reason for stopping retrieving or handling the candidate records
151
+ // - only one stopReason is returned (if there would be several possible stopReasons, stopReason is picked in the above order)
152
+ // - 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
153
+
154
+ function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount, nonMatchCount}) {
155
+ checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount});
156
+ const matchStatus = getMatchState(state, stopReason);
157
+ // add nonMatches to result only if returnNonMatches is 'true', otherwise nonMatches have not been gathered
158
+ const result = returnNonMatches ? {matches, matchStatus, nonMatches} : {matches, matchStatus};
159
+ debugData(`${JSON.stringify(result)}`);
160
+ return result;
161
+
162
+ // note that in cases where the matching has been stopped because of maxMatches checkCounts won't (in most cases) match
163
+
164
+ function checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount}) {
165
+ const matchCount = matches.length;
166
+ debugData(`Return nonMatches: ${returnNonMatches}`);
167
+ const chosenNonMatchCount = returnNonMatches ? nonMatches.length : nonMatchCount;
168
+ const totalHandled = matchCount + nonMatchCount + duplicateCount;
169
+ debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}`);
170
+ debug(`We got result for ${totalHandled} / ${candidateCount} retrieved candidates`);
171
+ if (totalHandled !== candidateCount) {
172
+ debug(`WARNING: Missing results for ${candidateCount - totalHandled} candidates`);
173
+ return;
174
+ }
175
+ return;
176
+ }
177
+
178
+ function getMatchState(state, stopReason) {
179
+ debugData(`${JSON.stringify(state)}`);
180
+ 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}`);
181
+
182
+ debugData(`StopReason: <${stopReason}>`);
183
+
184
+ const searchesLeft = state.resultSetOffset && state.resultSetOffset <= state.totalRecords;
185
+ const nonRetrieved = searchesLeft ? state.totalRecords - state.queryCandidateCounter : 0;
186
+ debugData(`nonRetrieved: ${nonRetrieved}`);
187
+
188
+ if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0) {
189
+ const maxedQueriesStopReason = state.maxedQueries.length > 0 ? 'maxedQueries' : '';
190
+ const newStopReason = stopReason === '' ? maxedQueriesStopReason : stopReason;
191
+ debugData(`MaxedQueriesStopReason: <${maxedQueriesStopReason}>`);
192
+ debugData(`NewStopReason: <${newStopReason}>`);
193
+ debug(`Match status: false`);
194
+ return {status: false, stopReason: newStopReason};
195
+ }
196
+
197
+ debug(`Match status: true`);
198
+ return {status: true, stopReason};
199
+ }
200
+ }
201
+
202
+ function iterateRecords({records, recordSetSize, maxMatches, matches = [], nonMatches = [], recordMatches = [], recordNonMatches = [], recordCount = 0, recordDuplicateCount = 0, recordNonMatchCount = 0}) {
203
+
204
+ // recordSetSize : total amount of records in the current record set
205
+ // recordCount : amount of records from the current record set that have been handled
206
+ // maxMatches : setting for maximum amount found by current matcher job before the matcher job is stopped
207
+ // recordDuplicateCount : amount of records from the current record set that are already included in matches/nonMatches results
208
+ // recordNonMatchCount: amount of records from the current record set that are nonMatches (only is returnNonMatches setting is false)
209
+
210
+ // records : non-handled records in the current record set
211
+ // matches : found matches in the current matcher job
212
+ // recordMatches : found matches in the current record set
213
+ // recordNonMatches : found nonMatches in the current record set (only if returnNonMatches setting is true)
214
+
215
+ const [candidate] = records;
216
+ const newRecordCount = candidate ? recordCount + 1 : recordCount;
217
+
218
+ // The matcher uses same matchDetection strategy for candidates from all candidate-searches -> matchDetection result for the same candidate is always same
219
+ // Exceptions would happen if the candidate would have been updated in the database between candidate searches
220
+ // 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
221
+ // different candidate search queries. Same candidate search query won't have duplicate records.
74
222
 
75
- function iterateRecords(records) {
76
- const [candidate] = records;
223
+ if (candidate) {
77
224
 
78
- if (candidate) {
225
+ if (candidateNotInMatches(matches.concat(nonMatches), candidate)) {
79
226
  const {record: candidateRecord, id: candidateId} = candidate;
80
- const {match, probability} = detect(record, candidateRecord);
81
-
82
- if (match) {
83
- return {
84
- probability,
85
- candidate: {
86
- id: candidateId,
87
- record: candidateRecord
88
- }
89
- };
90
- }
91
-
92
- return iterateRecords(records.slice(1));
227
+ debug(`Running matchDetection for record ${candidateId} (${newRecordCount}/${recordSetSize})`);
228
+ const detectionResult = detect(record, candidateRecord);
229
+ return handleDetectionResult(detectionResult, candidateId, candidateRecord);
230
+ }
231
+ return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount: recordDuplicateCount + 1, recordNonMatchCount});
232
+ }
233
+
234
+ debug(`No more candidates, record set (${recordCount}/${recordSetSize}) done, ${recordMatches.length} matches found, ${recordDuplicateCount} candidates already handled, ${returnNonMatches ? `${recordNonMatches.length}` : `${recordNonMatchCount}`} nonMatches found.`);
235
+ return {matches: recordMatches, nonMatches: returnNonMatches ? recordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: recordNonMatchCount};
236
+
237
+ function handleDetectionResult(detectionResult, candidateId, candidateRecord) {
238
+ debugData(`MatchDetection results for ${candidateId} (${newRecordCount}/${recordSetSize}): ${JSON.stringify(detectionResult)}`);
239
+
240
+ if (detectionResult.match || returnNonMatches) {
241
+ debug(`${detectionResult.match ? `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is a match!` : `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is NOT a match!`}`);
242
+ debugData(`Strategy: ${JSON.stringify(detectionResult.strategy)}, Treshold: ${JSON.stringify(detectionResult.treshold)}`);
243
+
244
+ const matchResult = {
245
+ probability: detectionResult.probability,
246
+ candidate: {
247
+ id: candidateId,
248
+ record: candidateRecord
249
+ }
250
+ };
251
+ const strategyResult = {
252
+ strategy: detectionResult.strategy,
253
+ treshold: detectionResult.treshold
254
+ };
255
+ const newMatch = returnStrategy ? {...matchResult, ...strategyResult} : {...matchResult};
256
+
257
+ debugData(`${JSON.stringify(newMatch)}`);
258
+
259
+ return handleRecordMatch(detectionResult.match, newMatch);
93
260
  }
261
+
262
+ const newRecordNonMatchCount = recordNonMatchCount + 1;
263
+ debugData(`- Total nonMatches after this detection: ${newRecordNonMatchCount}`);
264
+
265
+ return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount});
266
+ }
267
+
268
+ function handleRecordMatch(isMatch, newMatch) {
269
+ const newRecordMatches = isMatch ? recordMatches.concat(newMatch) : recordMatches;
270
+ const newRecordNonMatches = isMatch ? recordNonMatches : recordNonMatches.concat(newMatch);
271
+
272
+ debugData(`- Total matches after this detection: ${matches.concat(newRecordMatches).length} (max: ${maxMatches})`);
273
+
274
+ // eslint-disable-next-line functional/no-conditional-statement
275
+ if (returnNonMatches) {
276
+ debugData(`- Total nonMatches after this detection: ${nonMatches.concat(newRecordNonMatches).length}`);
277
+ }
278
+
279
+ if (maxMatchesFound({matches: matches.concat(newRecordMatches), maxMatches})) {
280
+ debug(`MaxMatches (${maxMatches}) reached, handled candidates in record set: ${newRecordCount} non-handled candidates in record set ${recordSetSize - newRecordCount}`);
281
+ return {matches: newRecordMatches, nonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: recordNonMatchCount};
282
+ }
283
+
284
+ return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, recordNonMatchCount});
285
+ }
286
+
287
+ function candidateNotInMatches(matches, candidate) {
288
+ debug(`Checking that record ${candidate.id} is not already included in ${matches.length} matches/nonMatches`);
289
+ const newCandidateId = candidate.id;
290
+ debugData(`newCandidateId: ${newCandidateId}`);
291
+ const result = matches.find(({candidate}) => candidate.id === newCandidateId);
292
+ debugData(`Result: ${result}`);
293
+ if (result) {
294
+ debug(`${candidate.id} was already handled.`);
295
+ return false;
296
+ }
297
+ debug(`${candidate.id} not found in matches/nonMatches`);
298
+ return true;
299
+ }
300
+ }
301
+
302
+ function maxMatchesFound({matches, maxMatches}) {
303
+ if (maxMatches && matches.length >= maxMatches) {
304
+ debug(`Stopping recordSet iteration: maxMatches (${maxMatches}) for matcher job found.`);
305
+ return true;
94
306
  }
95
307
  }
96
308
  };
package/src/index.spec.js CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Melinda record matching modules for Javascript
6
6
  *
7
- * Copyright (C) 2020 University Of Helsinki (The National Library Of Finland)
7
+ * Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)
8
8
  *
9
9
  * This file is part of melinda-record-matching-js
10
10
  *
@@ -31,6 +31,10 @@ import {READERS} from '@natlibfi/fixura';
31
31
  import generateTests from '@natlibfi/fixugen-http-client';
32
32
  import {MarcRecord} from '@natlibfi/marc-record';
33
33
  import createMatchInterface, {matchDetection} from '.';
34
+ import createDebugLogger from 'debug';
35
+
36
+ const debug = createDebugLogger('@natlibfi/melinda-record-matching:index:test');
37
+ const debugData = debug.extend('data');
34
38
 
35
39
  describe('INDEX', () => {
36
40
  generateTests({
@@ -42,19 +46,31 @@ describe('INDEX', () => {
42
46
  }
43
47
  });
44
48
 
45
- async function callback({getFixture, options, enabled = true}) {
49
+ async function callback({getFixture, options, enabled = true, expectedMatchStatus, expectedStopReason}) {
46
50
 
47
51
  if (!enabled) {
52
+ debug(`Disabled test!`);
48
53
  return;
49
54
  }
50
55
 
51
56
  const record = new MarcRecord(getFixture('inputRecord.json'), {subfieldValues: false});
52
57
  const expectedMatches = getFixture('expectedMatches.json');
58
+ const expectedNonMatches = getFixture('expectedNonMatches.json') || [];
59
+
53
60
 
54
61
  const match = createMatchInterface(formatOptions());
55
- const matches = await match(record);
62
+ const {matches, matchStatus, nonMatches} = await match(record);
63
+ debugData(`${matches.length}, ${matchStatus.status}/${matchStatus.stopReason}, ${nonMatches ? nonMatches.length : nonMatches}`);
64
+
65
+ const formattedMatchResult = formatRecordResults(matches);
66
+ expect(formattedMatchResult).to.eql(expectedMatches);
67
+
68
+ const formattedNonMatchResult = formatRecordResults(nonMatches);
69
+ expect(formattedNonMatchResult).to.eql(expectedNonMatches);
70
+
71
+ expect(matchStatus.status).to.eql(expectedMatchStatus);
72
+ expect(matchStatus.stopReason).to.eql(expectedStopReason);
56
73
 
57
- expect(formatResults()).to.eql(expectedMatches);
58
74
 
59
75
  function formatOptions() {
60
76
  const contextFeatures = matchDetection.features[options.detection.strategy.type];
@@ -62,26 +78,37 @@ describe('INDEX', () => {
62
78
  return {
63
79
  ...options,
64
80
  search: {
65
- ...options.search,
66
- maxRecordsPerRequest: 1
81
+ ...options.search
67
82
  },
68
83
  detection: {
69
84
  ...options.detect,
70
85
  strategy: options.detection.strategy.features.map(v => contextFeatures[v]())
71
- },
72
- maxMatches: 2,
73
- maxCandidates: 1
86
+ }
74
87
  };
75
88
  }
76
89
 
77
- function formatResults() {
78
- return matches.map(({candidate, probability}) => ({
79
- probability,
80
- candidate: {
81
- id: candidate.id,
82
- record: candidate.record.toObject()
83
- }
84
- }));
90
+ function formatRecordResults(matches) {
91
+ if (matches) {
92
+ debugData(JSON.stringify(matches));
93
+ return matches.map((match) => ({
94
+ ...match,
95
+ candidate: formatCandidate(match.candidate)
96
+ }));
97
+ }
98
+ return [];
85
99
  }
100
+
101
+ // Format candidate to remove validationOptions from record
102
+ function formatCandidate({id, record}) {
103
+ const newId = id;
104
+ const newRecord = record;
105
+ return {
106
+ id: newId,
107
+ record: newRecord.toObject()
108
+ };
109
+ }
110
+
111
+
86
112
  }
113
+
87
114
  });
@@ -33,11 +33,16 @@ import * as features from './features';
33
33
  export {features};
34
34
 
35
35
  // eslint-disable-next-line max-statements
36
- export default ({strategy, treshold = 0.9}) => (recordA, recordB) => {
36
+ export default ({strategy, treshold = 0.9}, returnStrategy = false) => (recordA, recordB) => {
37
37
  const minProbabilityQuantifier = 0.5;
38
38
 
39
39
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection');
40
40
  const debugData = debug.extend('data');
41
+
42
+ debugData(`Strategy: ${JSON.stringify(strategy)}`);
43
+ debugData(`Treshold: ${JSON.stringify(treshold)}`);
44
+ debugData(`ReturnStrategy: ${JSON.stringify(returnStrategy)}`);
45
+
41
46
  const featuresA = extractFeatures(recordA);
42
47
  const featuresB = extractFeatures(recordB);
43
48
 
@@ -50,11 +55,26 @@ export default ({strategy, treshold = 0.9}) => (recordA, recordB) => {
50
55
  if (similarityVector.some(v => v >= minProbabilityQuantifier)) {
51
56
  const probability = calculateprobability();
52
57
  debug(`probability: ${probability} (Treshold: ${treshold})`);
53
- return {match: probability >= treshold, probability};
58
+ return returnResult({match: probability >= treshold, probability});
54
59
  }
55
60
 
56
61
  debugData(`No feature yielded minimum probability amount of points (${minProbabilityQuantifier})`);
57
- return {match: false, probability: 0.0};
62
+ return returnResult({match: false, probability: 0.0});
63
+
64
+ function returnResult(result) {
65
+ if (returnStrategy) {
66
+ debug(`Returning detection strategy with the result`);
67
+ const resultWithStrategy = {match: result.match, probability: result.probability, strategy: formatStrategy(strategy), treshold};
68
+ debugData(`${JSON.stringify(resultWithStrategy)}`);
69
+ return resultWithStrategy;
70
+ }
71
+ return result;
72
+ }
73
+
74
+ function formatStrategy(strategy) {
75
+ const strategyNames = strategy.map(element => element.name);
76
+ return strategyNames || [];
77
+ }
58
78
 
59
79
  function calculateprobability() {
60
80
  const probability = similarityVector.reduce((acc, v) => acc + v, 0.0);