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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,6 +31,7 @@ import createClient, {SruSearchError} from '@natlibfi/sru-client';
31
31
  import {MarcRecord} from '@natlibfi/marc-record';
32
32
  import {MARCXML} from '@natlibfi/marc-record-serializers';
33
33
  import generateQueryList from './query-list';
34
+ import {Error as MatchingError} from '@natlibfi/melinda-commons';
34
35
 
35
36
  export {searchTypes} from './query-list';
36
37
 
@@ -39,7 +40,7 @@ export class CandidateSearchError extends Error {}
39
40
  // serverMaxResults : maximum size of total search result available from the server, defaults to Aleph's 20000
40
41
 
41
42
  // eslint-disable-next-line max-statements
42
- export default ({record, searchSpec, url, maxRecordsPerRequest = 50, serverMaxResult = 20000}) => {
43
+ export default ({record, searchSpec, url, maxCandidates, maxRecordsPerRequest = 50, serverMaxResult = 20000}) => {
43
44
  MarcRecord.setValidationOptions({subfieldValues: false});
44
45
 
45
46
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search');
@@ -49,11 +50,16 @@ export default ({record, searchSpec, url, maxRecordsPerRequest = 50, serverMaxRe
49
50
  debugData(`Url: ${url}`);
50
51
  debugData(`MaxRecordsPerRequest ${maxRecordsPerRequest}`);
51
52
  debugData(`ServerMaxResult: ${serverMaxResult}`);
53
+ debugData(`MaxCandidates: ${maxCandidates}`);
54
+
55
+ // Do not retrieve more candidates than defined in maxCandidates
56
+ const adjustedMaxRecordsPerRequest = maxRecordsPerRequest >= maxCandidates ? maxCandidates : maxRecordsPerRequest;
52
57
 
53
58
  const inputRecordId = getRecordId(record);
54
59
  const queryList = generateQueryList(record, searchSpec);
55
60
  const client = createClient({
56
- url, maxRecordsPerRequest,
61
+ url,
62
+ maxRecordsPerRequest: adjustedMaxRecordsPerRequest,
57
63
  version: '2.0',
58
64
  retrieveAll: false
59
65
  });
@@ -67,39 +73,39 @@ export default ({record, searchSpec, url, maxRecordsPerRequest = 50, serverMaxRe
67
73
  // state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)
68
74
  // state.query : current query (undefined if there was no queries left)
69
75
  // state.searchCounter : sequence for current search for current query (undefined, if there we no queries left)
70
- // 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)
76
+ // state.queryCandidateCounter: amount of candidates (records+failures) retrieved from SRU for matching for current query, including the current record+failure set (undefined if there were no queries left)
71
77
  // state.queriesLeft : amount of queries left
72
78
  // state.queryCounter : sequence for current query
73
79
  // state.maxedQueries : queries that resulted in more than serverMaxResults hits
74
80
 
75
81
 
76
- // eslint-disable-next-line max-statements
77
82
  return async ({queryOffset = 0, resultSetOffset = 1, totalRecords = 0, searchCounter = 0, queryCandidateCounter = 0, queryCounter = 0, maxedQueries = []}) => {
78
83
  const query = queryList[queryOffset];
79
84
 
80
85
  if (query) {
81
- const {records, nextOffset, total} = await retrieveRecords();
86
+ const {records, failures, nextOffset, total} = await retrieveRecords();
82
87
 
83
88
  // If resultSetOffset === 1 this is the first search for the current query
89
+ debugData(`ResultSetOffset: ${resultSetOffset}`);
84
90
  const newTotalRecords = resultSetOffset === 1 ? total : totalRecords;
85
91
  const newQueryCounter = resultSetOffset === 1 ? queryCounter + 1 : queryCounter;
86
92
  const newSearchCounter = resultSetOffset === 1 ? 1 : searchCounter + 1;
87
- const newQueryCandidateCounter = resultSetOffset === 1 ? records.length : queryCandidateCounter + records.length;
93
+ const newQueryCandidateCounter = resultSetOffset === 1 ? records.length + failures.length : queryCandidateCounter + records.length + failures.length;
88
94
 
89
95
  const maxedQuery = resultSetOffset === 1 ? checkMaxedQuery(query, total, serverMaxResult) : undefined;
90
96
  const newMaxedQueries = maxedQuery ? maxedQueries.concat(maxedQuery) : maxedQueries;
91
97
 
92
98
  if (typeof nextOffset === 'number') {
93
99
  debug(`Next search will be for query ${queryOffset} ${query}, starting from record ${nextOffset}`);
94
- return {records, queryOffset, resultSetOffset: nextOffset, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCandidateCounter, maxedQueries: newMaxedQueries};
100
+ return {records, failures, queryOffset, resultSetOffset: nextOffset, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCounter, maxedQueries: newMaxedQueries};
95
101
  }
96
102
  debug(`Query ${queryOffset} ${query} done.`);
97
103
  debug(`There are (${queryList.length - (queryOffset + 1)} queries left)`);
98
- return {records, queryOffset: queryOffset + 1, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCounter, maxedQueries: newMaxedQueries};
104
+ return {records, failures, queryOffset: queryOffset + 1, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCounter, maxedQueries: newMaxedQueries};
99
105
  }
100
106
 
101
107
  debug(`All ${queryList.length} queries done, there's no query for ${queryOffset}`);
102
- return {records: [], queriesLeft: 0, queryCounter, maxedQueries};
108
+ return {records: [], failures: [], queriesLeft: 0, queryCounter, maxedQueries};
103
109
 
104
110
  function retrieveRecords() {
105
111
  return new Promise((resolve, reject) => {
@@ -125,35 +131,39 @@ export default ({record, searchSpec, url, maxRecordsPerRequest = 50, serverMaxRe
125
131
  })
126
132
  .on('end', async nextOffset => {
127
133
  try {
128
- const records = await Promise.all(promises);
129
- const filtered = records.filter(r => r);
134
+ const recordPromises = await Promise.allSettled(promises);
135
+ debug(`All recordPromises: ${JSON.stringify(recordPromises)}`);
136
+ const filtered = recordPromises.filter(r => r.status === 'fulfilled').map(r => r.value);
137
+ const failures = recordPromises.filter(r => r.status === 'rejected').map(r => ({status: r.reason.status, payload: r.reason.payload}));
138
+
139
+ debug(`Found ${JSON.stringify(recordPromises)} records`);
140
+ debug(`Found ${filtered.length} convertable candidates`);
141
+ debug(`Found ${failures.length} NON-convertable candidates`);
142
+ debug(`Converted: ${JSON.stringify(filtered)}.`);
143
+ debug(`Not converted: ${JSON.stringify(failures)}.`);
130
144
 
131
- debug(`Found ${filtered.length} candidates`);
132
145
 
133
- resolve({nextOffset, records: filtered, total: totalRecords});
146
+ resolve({nextOffset, records: filtered, failures, total: totalRecords});
134
147
  } catch (err) {
148
+ debug(`Error caught on END`);
135
149
  reject(err);
136
150
  }
137
151
  })
138
- .on('record', foundRecord => {
152
+ .on('record', foundRecordXML => {
139
153
  promises.push(handleRecord()); // eslint-disable-line functional/immutable-data
140
154
 
141
155
  async function handleRecord() {
142
156
  try {
143
- const foundRecordMarc = await MARCXML.from(foundRecord, {subfieldValues: false});
157
+ const foundRecordMarc = await MARCXML.from(foundRecordXML, {subfieldValues: false});
144
158
  const foundRecordId = getRecordId(foundRecordMarc);
145
159
 
146
- // This does not work and might cause problems:
147
- // Record *should* match itself AND in REST the input record is given id 000000001 always
148
- debug(`Checking record id's - this does not work ${inputRecordId} vs ${foundRecordId}`);
149
- if (inputRecordId === foundRecordId) {
150
- debug(`Input and candidate are the same record per 001. Discarding candidate`);
151
- return;
152
- }
153
-
154
160
  return {record: foundRecordMarc, id: foundRecordId};
155
161
  } catch (err) {
156
- throw new Error(`Failed converting record: ${err}, record: ${foundRecord}`);
162
+ // 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});
157
167
  }
158
168
  }
159
169
  });
@@ -174,4 +184,11 @@ export default ({record, searchSpec, url, maxRecordsPerRequest = 50, serverMaxRe
174
184
  const [field] = record.get(/^001$/u);
175
185
  return field ? field.value : '';
176
186
  }
187
+
188
+ function getRecordIdFromXML(recordXML) {
189
+ //<controlfield tag=\"001\">015376846</controlfield
190
+ debug(`Cannot yet find possible database record id from recordXML (length ${recordXML.length})`);
191
+ return undefined;
192
+ }
193
+
177
194
  };
@@ -30,6 +30,7 @@ import {expect} from 'chai';
30
30
  import {READERS} from '@natlibfi/fixura';
31
31
  import generateTests from '@natlibfi/fixugen-http-client';
32
32
  import {MarcRecord} from '@natlibfi/marc-record';
33
+ import {Error as MatchingError} from '@natlibfi/melinda-commons';
33
34
  import createSearchInterface, {CandidateSearchError} from '.';
34
35
  import createDebugLogger from 'debug';
35
36
 
@@ -46,7 +47,7 @@ describe('candidate-search', () => {
46
47
  });
47
48
 
48
49
  // eslint-disable-next-line max-statements
49
- async function callback({getFixture, factoryOptions, searchOptions, expectedFactoryError, expectedSearchError, enabled = true}) {
50
+ async function callback({getFixture, factoryOptions, searchOptions, expectedFactoryError = false, expectedSearchError = false, enabled = true}) {
50
51
  const url = 'http://foo.bar';
51
52
 
52
53
  if (!enabled) {
@@ -64,7 +65,7 @@ describe('candidate-search', () => {
64
65
  }
65
66
 
66
67
  const search = createSearchInterface({...formatFactoryOptions(), url});
67
- await iterate({searchOptions});
68
+ await iterate({searchOptions, expectedSearchError});
68
69
 
69
70
  function formatFactoryOptions() {
70
71
  debug(`Using factoryOptions: ${JSON.stringify(factoryOptions)}`);
@@ -77,7 +78,7 @@ describe('candidate-search', () => {
77
78
  }
78
79
 
79
80
  // eslint-disable-next-line max-statements
80
- async function iterate({searchOptions, count = 1}) {
81
+ async function iterate({searchOptions, expectedSearchError, expectedErrorStatus, count = 1}) {
81
82
  const expectedResults = getFixture(`expectedResults${count}.json`);
82
83
 
83
84
  if (expectedSearchError) { // eslint-disable-line functional/no-conditional-statement
@@ -85,36 +86,35 @@ describe('candidate-search', () => {
85
86
  await search(searchOptions);
86
87
  throw new Error('Expected an error');
87
88
  } catch (err) {
89
+ debug(`Got an error: ${err}`);
88
90
  expect(err).to.be.an('error');
89
- expect(err.message).to.match(new RegExp(expectedSearchError, 'u'));
91
+ const errorMessage = err instanceof MatchingError ? err.payload.message : err.message;
92
+ const errorStatus = err instanceof MatchingError ? err.status : undefined;
93
+ debug(`errorMessage: ${errorMessage}, errorStatus: ${errorStatus}`);
94
+ expect(errorMessage).to.match(new RegExp(expectedSearchError, 'u'));
95
+
96
+ if (expectedErrorStatus) {
97
+ expect(errorStatus).to.be(expectedErrorStatus);
98
+ return;
99
+ }
90
100
  return;
91
101
  }
92
102
  }
93
103
 
94
- const results = await search(searchOptions);
95
-
96
- expect(formatResults(results)).to.eql(expectedResults);
97
-
98
- if (results.records.length > 0) {
99
- return iterate({
100
- searchOptions: resultsToOptions(results),
101
- count: count + 1
102
- });
104
+ // eslint-disable-next-line functional/no-conditional-statement
105
+ if (!expectedSearchError) {
106
+ const results = await search(searchOptions);
107
+ expect(formatResults(results)).to.eql(expectedResults);
103
108
  }
104
109
 
105
110
  function formatResults(results) {
106
- // console.log(results); //eslint-disable-line
111
+ debug(results); //eslint-disable-line
107
112
  return {
108
113
  ...results,
109
114
  records: results.records.map(({record, id}) => ({id, record: record.toObject()}))
110
115
  };
111
116
  }
112
117
 
113
- function resultsToOptions(results) {
114
- return Object.entries(results)
115
- .filter(([k]) => k === 'records' === false) // If key is 'records' return false
116
- .reduce((acc, [k, v]) => ({...acc, [k]: v}), {});
117
- }
118
118
  }
119
119
  }
120
120
  });
@@ -175,12 +175,15 @@ export function bibHostComponents(record) {
175
175
 
176
176
  // SRU search dc.title with a search phrase starting with ^ maps currently in Melinda to
177
177
  // (probably) to *headings* index TIT
178
+ // - Aleph cannot currently handle headings searches starting with a boolean - in these cases use word search
179
+
178
180
  // Headings index TIT drops articles etc. from the start of the title according to the filing indicator
179
181
  // Currently filing indicator is not implemented - if the title starts with an article and the Melinda
180
182
  // record is correctly catalogued using a filing indicator -> dc.title search won't match
181
183
 
182
184
  export function bibTitle(record) {
183
185
  const title = getTitle();
186
+ const booleanStartWords = ['and', 'or', 'nor', 'not'];
184
187
 
185
188
  if (title) {
186
189
  const formatted = title
@@ -191,8 +194,10 @@ export function bibTitle(record) {
191
194
  .slice(0, 30)
192
195
  .trim();
193
196
 
197
+ // use word search for titles starting with a boolean
198
+ const useWordSearch = booleanStartWords.some(word => formatted.toLowerCase().startsWith(word));
194
199
  // Prevent too many matches by having a minimum length requirement
195
- return formatted.length >= 5 ? [`dc.title="^${formatted}*"`] : [];
200
+ return formatted.length >= 5 ? [`dc.title="${useWordSearch ? '' : '^'}${formatted}*"`] : [];
196
201
  }
197
202
 
198
203
  return [];
package/src/index.js CHANGED
@@ -32,7 +32,7 @@ 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, returnStrategy = false, returnQuery = false, returnNonMatches = false}) => {
35
+ export default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false, returnFailures}) => {
36
36
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:index');
37
37
  const debugData = debug.extend('data');
38
38
 
@@ -43,11 +43,13 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
43
43
  debugData(`ReturnStrategy: ${JSON.stringify(returnStrategy)}`);
44
44
  debugData(`ReturnQuery: ${JSON.stringify(returnQuery)}`);
45
45
  debugData(`ReturnNonMatches: ${JSON.stringify(returnNonMatches)}`);
46
+ debugData(`ReturnFailures: ${JSON.stringify(returnFailures)}`);
47
+
46
48
 
47
49
  const detect = createDetectionInterface(detectionOptions, returnStrategy);
48
50
 
49
51
  return record => {
50
- const search = createSearchInterface({...searchOptions, record});
52
+ const search = createSearchInterface({...searchOptions, record, maxCandidates});
51
53
  return iterate({});
52
54
 
53
55
  // candidateCount : amount of candidate records retrived from SRU for matching, NOT including current record set
@@ -63,13 +65,17 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
63
65
  // state.queryCounter : sequence for current query
64
66
  // state.maxedQueries : queries that resulted in more than serverMaxResults hits
65
67
 
66
- async function iterate({initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0}) {
68
+ async function iterate({initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0, nonMatchCount = 0, conversionFailures = []}) {
67
69
  debugData(`Starting next matcher iteration.`);
68
- const {records, ...state} = await search(initialState);
70
+ const {records, failures, ...state} = await search(initialState);
69
71
 
70
- debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}`);
72
+ debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, nonMatchCount: ${nonMatchCount}, conversionFailures: ${conversionFailures}`);
71
73
  const recordSetSize = records.length;
72
- const newCandidateCount = candidateCount + recordSetSize;
74
+ const failureSetSize = failures.length;
75
+ const newCandidateCount = candidateCount + recordSetSize + failureSetSize;
76
+
77
+ const newConversionFailures = conversionFailures.concat(failures);
78
+ debugData(`Failures: ${failures.length}, ConversionFailures: ${conversionFailures.length}, NewConversionFailures: ${newConversionFailures.length}`);
73
79
 
74
80
  if (recordSetSize > 0) {
75
81
  return handleRecordSet();
@@ -77,28 +83,30 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
77
83
 
78
84
  if (state.queriesLeft > 0) {
79
85
  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});
86
+ return iterate({initialState: state, matches, candidateCount: newCandidateCount, nonMatches, nonMatchCount, duplicateCount, conversionFailures: newConversionFailures});
81
87
  }
82
88
 
83
89
  debug(`No (more) candidate records to check, no more queries left, matches: ${matches.length}`);
84
- return returnResult({matches, state, stopReason: '', nonMatches, candidateCount: newCandidateCount, duplicateCount});
90
+ return returnResult({matches, state, stopReason: '', nonMatches, nonMatchCount, candidateCount: newCandidateCount, duplicateCount, conversionFailures: newConversionFailures});
85
91
 
86
92
  function handleRecordSet() {
87
93
  debug(`Checking record set of ${recordSetSize} candidate records for possible matches, found by ${state.searchCounter} search for ${state.query}`);
88
94
 
89
- const matchResult = iterateRecords({records, recordSetSize, maxMatches, matches, nonMatches});
95
+ const matchResult = iterateRecords({records, recordSetSize, maxMatches, matches, nonMatches, nonMatchCount});
96
+
90
97
  const newDuplicateCount = duplicateCount + matchResult.duplicateCount;
98
+ const newNonMatchCount = nonMatchCount + matchResult.nonMatchCount;
91
99
  const {newMatches, newNonMatches} = handleMatchResult(matchResult, matches, nonMatches);
92
100
 
93
101
  if (maxMatchesFound({matches: newMatches, maxMatches})) {
94
- return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount});
102
+ return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures});
95
103
  }
96
104
 
97
105
  if (maxCandidatesRetrieved(newCandidateCount, maxCandidates)) {
98
- return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount});
106
+ return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures});
99
107
  }
100
108
 
101
- return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount});
109
+ return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures});
102
110
  }
103
111
 
104
112
  function handleMatchResult(matchResult, matches, nonMatches) {
@@ -146,26 +154,29 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
146
154
  // we could have here also returnRecords/returnMatchRecords/returnNonMatchRecord options that could be turned false for not to return actual record data
147
155
 
148
156
  // matchStatus.status: boolean, true if matcher retrieved and handled all found candidate records, false if it did not
149
- // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries',empty string/undefined), reason for stopping retrieving or handling the candidate records
157
+ // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries','conversionFailures', empty string/undefined), reason for stopping retrieving or handling the candidate records
150
158
  // - only one stopReason is returned (if there would be several possible stopReasons, stopReason is picked in the above order)
151
159
  // - 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
152
160
 
153
- function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount}) {
154
- checkCounts(matches, nonMatches, candidateCount, duplicateCount);
155
- const matchStatus = getMatchState(state, stopReason);
161
+ function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount, nonMatchCount, conversionFailures}) {
162
+ const conversionFailureCount = conversionFailures.length;
163
+ checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, conversionFailureCount});
164
+ const matchStatus = getMatchState(state, stopReason, conversionFailureCount);
156
165
  // add nonMatches to result only if returnNonMatches is 'true', otherwise nonMatches have not been gathered
157
- const result = returnNonMatches ? {matches, matchStatus, nonMatches} : {matches, matchStatus};
166
+ const matchesResult = returnNonMatches ? {matches, matchStatus, nonMatches} : {matches, matchStatus};
167
+ const result = returnFailures ? {...matchesResult, conversionFailures} : matchesResult;
168
+ debugData(`ReturnFailures ${returnFailures}`);
158
169
  debugData(`${JSON.stringify(result)}`);
159
170
  return result;
160
171
 
161
- // we could have a nonMatchCount even if nonMatches won't be returned so that checkCounts would make sense
162
172
  // note that in cases where the matching has been stopped because of maxMatches checkCounts won't (in most cases) match
163
173
 
164
- function checkCounts(matches, nonMatches, candidateCount, duplicateCount) {
174
+ function checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, conversionFailureCount}) {
165
175
  const matchCount = matches.length;
166
- const nonMatchCount = nonMatches.length;
167
- const totalHandled = matchCount + nonMatchCount + duplicateCount;
168
- debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${nonMatchCount}, duplicateCount: ${duplicateCount}`);
176
+ debugData(`Return nonMatches: ${returnNonMatches}`);
177
+ const chosenNonMatchCount = returnNonMatches ? nonMatches.length : nonMatchCount;
178
+ const totalHandled = matchCount + chosenNonMatchCount + duplicateCount;
179
+ debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}, conversionFailureCount: ${conversionFailureCount}`);
169
180
  debug(`We got result for ${totalHandled} / ${candidateCount} retrieved candidates`);
170
181
  if (totalHandled !== candidateCount) {
171
182
  debug(`WARNING: Missing results for ${candidateCount - totalHandled} candidates`);
@@ -174,16 +185,26 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
174
185
  return;
175
186
  }
176
187
 
177
- function getMatchState(state, stopReason) {
178
- debug(`Queries left ${state.queriesLeft}, Searches for current query left: ${state.resultSetOffset >= state.totalRecords}, non-retrieved records: ${state.totalRecords - state.queryCandidateCounter}, maxedQueries (${state.maxedQueries.length}): ${state.maxedQueries}`);
188
+ function getMatchState(state, stopReason, conversionFailuresCount) {
189
+ debugData(`${JSON.stringify(state)}`);
190
+ debug(`We had ${conversionFailuresCount} retrieved candidates that could not be converted.`);
191
+ 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
+
179
193
  debugData(`StopReason: <${stopReason}>`);
180
194
 
181
- const nonRetrieved = state.resultSetOffset >= state.totalRecords ? state.totalRecords - state.queryCandidateCounter : 0;
195
+ const searchesLeft = state.resultSetOffset && state.resultSetOffset <= state.totalRecords;
196
+ const nonRetrieved = searchesLeft ? state.totalRecords - state.queryCandidateCounter : 0;
197
+ debugData(`nonRetrieved: ${nonRetrieved}`);
182
198
 
183
- if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0) {
184
- const maxedQueriesStopReason = state.maxedQueries.length > 0 ? 'maxedQueries' : '';
185
- const newStopReason = stopReason === '' ? maxedQueriesStopReason : stopReason;
199
+ // 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
201
+
202
+ if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0 || conversionFailureCount > 0) {
203
+ const maxedQueriesStopReason = state.maxedQueries.length > 0 ? 'maxedQueries' : undefined;
204
+ const conversionFailuresStopReason = conversionFailureCount > 0 ? 'conversionFailures' : undefined;
205
+ const newStopReason = stopReason === '' || stopReason === undefined ? maxedQueriesStopReason || conversionFailuresStopReason : stopReason;
186
206
  debugData(`MaxedQueriesStopReason: <${maxedQueriesStopReason}>`);
207
+ debugData(`ConversionFailureStopReason <${conversionFailuresStopReason}>`);
187
208
  debugData(`NewStopReason: <${newStopReason}>`);
188
209
  debug(`Match status: false`);
189
210
  return {status: false, stopReason: newStopReason};
@@ -194,12 +215,13 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
194
215
  }
195
216
  }
196
217
 
197
- function iterateRecords({records, recordSetSize, maxMatches, matches = [], nonMatches = [], recordMatches = [], recordNonMatches = [], recordCount = 0, recordDuplicateCount = 0}) {
218
+ function iterateRecords({records, recordSetSize, maxMatches, matches = [], nonMatches = [], recordMatches = [], recordNonMatches = [], recordCount = 0, recordDuplicateCount = 0, recordNonMatchCount = 0}) {
198
219
 
199
220
  // recordSetSize : total amount of records in the current record set
200
221
  // recordCount : amount of records from the current record set that have been handled
201
222
  // maxMatches : setting for maximum amount found by current matcher job before the matcher job is stopped
202
223
  // recordDuplicateCount : amount of records from the current record set that are already included in matches/nonMatches results
224
+ // recordNonMatchCount: amount of records from the current record set that are nonMatches (only is returnNonMatches setting is false)
203
225
 
204
226
  // records : non-handled records in the current record set
205
227
  // matches : found matches in the current matcher job
@@ -222,11 +244,11 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
222
244
  const detectionResult = detect(record, candidateRecord);
223
245
  return handleDetectionResult(detectionResult, candidateId, candidateRecord);
224
246
  }
225
- return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount: recordDuplicateCount + 1});
247
+ return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount: recordDuplicateCount + 1, recordNonMatchCount});
226
248
  }
227
249
 
228
- debug(`No more candidates, record set (${recordCount}/${recordSetSize}) done, ${recordMatches.length} matches found, ${recordDuplicateCount} candidates already handled${returnNonMatches ? `, ${recordNonMatches.length} nonMatches found.` : '.'}`);
229
- return {matches: recordMatches, nonMatches: returnNonMatches ? recordNonMatches : [], duplicateCount: recordDuplicateCount};
250
+ 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};
230
252
 
231
253
  function handleDetectionResult(detectionResult, candidateId, candidateRecord) {
232
254
  debugData(`MatchDetection results for ${candidateId} (${newRecordCount}/${recordSetSize}): ${JSON.stringify(detectionResult)}`);
@@ -252,12 +274,17 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
252
274
 
253
275
  return handleRecordMatch(detectionResult.match, newMatch);
254
276
  }
255
- return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount});
277
+
278
+ const newRecordNonMatchCount = recordNonMatchCount + 1;
279
+ debugData(`- Total nonMatches after this detection: ${newRecordNonMatchCount}`);
280
+
281
+ return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount});
256
282
  }
257
283
 
258
284
  function handleRecordMatch(isMatch, newMatch) {
259
285
  const newRecordMatches = isMatch ? recordMatches.concat(newMatch) : recordMatches;
260
286
  const newRecordNonMatches = isMatch ? recordNonMatches : recordNonMatches.concat(newMatch);
287
+ const newRecordNonMatchCount = isMatch ? recordNonMatchCount : recordNonMatchCount + 1;
261
288
 
262
289
  debugData(`- Total matches after this detection: ${matches.concat(newRecordMatches).length} (max: ${maxMatches})`);
263
290
 
@@ -265,13 +292,14 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
265
292
  if (returnNonMatches) {
266
293
  debugData(`- Total nonMatches after this detection: ${nonMatches.concat(newRecordNonMatches).length}`);
267
294
  }
295
+ debugData(`- Total nonMatchCount after this detection: ${recordNonMatchCount}`);
268
296
 
269
297
  if (maxMatchesFound({matches: matches.concat(newRecordMatches), maxMatches})) {
270
298
  debug(`MaxMatches (${maxMatches}) reached, handled candidates in record set: ${newRecordCount} non-handled candidates in record set ${recordSetSize - newRecordCount}`);
271
- return {matches: newRecordMatches, nonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount};
299
+ return {matches: newRecordMatches, nonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: newRecordNonMatchCount};
272
300
  }
273
301
 
274
- return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount});
302
+ return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount});
275
303
  }
276
304
 
277
305
  function candidateNotInMatches(matches, candidate) {
package/src/index.spec.js CHANGED
@@ -46,7 +46,7 @@ describe('INDEX', () => {
46
46
  }
47
47
  });
48
48
 
49
- async function callback({getFixture, options, enabled = true, expectedMatchStatus, expectedStopReason}) {
49
+ async function callback({getFixture, options, enabled = true, expectedMatchStatus, expectedStopReason, expectedFailures}) {
50
50
 
51
51
  if (!enabled) {
52
52
  debug(`Disabled test!`);
@@ -59,8 +59,11 @@ describe('INDEX', () => {
59
59
 
60
60
 
61
61
  const match = createMatchInterface(formatOptions());
62
- const {matches, matchStatus, nonMatches} = await match(record);
63
- debugData(`${matches.length}, ${matchStatus.status}/${matchStatus.stopReason}, ${nonMatches ? nonMatches.length : nonMatches}`);
62
+ const {matches, matchStatus, nonMatches, conversionFailures} = await match(record);
63
+ debugData(`Matches: ${matches.length}, Status: ${matchStatus.status}/${matchStatus.stopReason}, NonMatches: ${nonMatches ? nonMatches.length : 'not returned'}, ConversionFailures: ${conversionFailures ? conversionFailures.length : 'not returned'}`);
64
+
65
+ expect(matchStatus.status).to.eql(expectedMatchStatus);
66
+ expect(matchStatus.stopReason).to.eql(expectedStopReason);
64
67
 
65
68
  const formattedMatchResult = formatRecordResults(matches);
66
69
  expect(formattedMatchResult).to.eql(expectedMatches);
@@ -68,9 +71,10 @@ describe('INDEX', () => {
68
71
  const formattedNonMatchResult = formatRecordResults(nonMatches);
69
72
  expect(formattedNonMatchResult).to.eql(expectedNonMatches);
70
73
 
71
- expect(matchStatus.status).to.eql(expectedMatchStatus);
72
- expect(matchStatus.stopReason).to.eql(expectedStopReason);
73
-
74
+ // eslint-disable-next-line functional/no-conditional-statement
75
+ if (expectedFailures) {
76
+ expect(conversionFailures).to.eql(expectedFailures);
77
+ }
74
78
 
75
79
  function formatOptions() {
76
80
  const contextFeatures = matchDetection.features[options.detection.strategy.type];