@natlibfi/melinda-record-matching 2.1.0 → 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.
@@ -21,6 +21,8 @@ var _marcRecordSerializers = require("@natlibfi/marc-record-serializers");
21
21
 
22
22
  var _queryList = _interopRequireWildcard(require("./query-list"));
23
23
 
24
+ var _melindaCommons = require("@natlibfi/melinda-commons");
25
+
24
26
  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); }
25
27
 
26
28
  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; }
@@ -98,11 +100,10 @@ var _default = ({
98
100
  } // state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)
99
101
  // state.query : current query (undefined if there was no queries left)
100
102
  // state.searchCounter : sequence for current search for current query (undefined, if there we no queries left)
101
- // 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)
103
+ // 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)
102
104
  // state.queriesLeft : amount of queries left
103
105
  // state.queryCounter : sequence for current query
104
106
  // state.maxedQueries : queries that resulted in more than serverMaxResults hits
105
- // eslint-disable-next-line max-statements
106
107
 
107
108
 
108
109
  return async ({
@@ -119,6 +120,7 @@ var _default = ({
119
120
  if (query) {
120
121
  const {
121
122
  records,
123
+ failures,
122
124
  nextOffset,
123
125
  total
124
126
  } = await retrieveRecords(); // If resultSetOffset === 1 this is the first search for the current query
@@ -127,7 +129,7 @@ var _default = ({
127
129
  const newTotalRecords = resultSetOffset === 1 ? total : totalRecords;
128
130
  const newQueryCounter = resultSetOffset === 1 ? queryCounter + 1 : queryCounter;
129
131
  const newSearchCounter = resultSetOffset === 1 ? 1 : searchCounter + 1;
130
- const newQueryCandidateCounter = resultSetOffset === 1 ? records.length : queryCandidateCounter + records.length;
132
+ const newQueryCandidateCounter = resultSetOffset === 1 ? records.length + failures.length : queryCandidateCounter + records.length + failures.length;
131
133
  const maxedQuery = resultSetOffset === 1 ? checkMaxedQuery(query, total, serverMaxResult) : undefined;
132
134
  const newMaxedQueries = maxedQuery ? maxedQueries.concat(maxedQuery) : maxedQueries;
133
135
 
@@ -135,6 +137,7 @@ var _default = ({
135
137
  debug(`Next search will be for query ${queryOffset} ${query}, starting from record ${nextOffset}`);
136
138
  return {
137
139
  records,
140
+ failures,
138
141
  queryOffset,
139
142
  resultSetOffset: nextOffset,
140
143
  queriesLeft: queryList.length - (queryOffset + 1),
@@ -151,6 +154,7 @@ var _default = ({
151
154
  debug(`There are (${queryList.length - (queryOffset + 1)} queries left)`);
152
155
  return {
153
156
  records,
157
+ failures,
154
158
  queryOffset: queryOffset + 1,
155
159
  queriesLeft: queryList.length - (queryOffset + 1),
156
160
  totalRecords: newTotalRecords,
@@ -165,6 +169,7 @@ var _default = ({
165
169
  debug(`All ${queryList.length} queries done, there's no query for ${queryOffset}`);
166
170
  return {
167
171
  records: [],
172
+ failures: [],
168
173
  queriesLeft: 0,
169
174
  queryCounter,
170
175
  maxedQueries
@@ -192,23 +197,34 @@ var _default = ({
192
197
  totalRecords += total;
193
198
  }).on('end', async nextOffset => {
194
199
  try {
195
- const records = await Promise.all(promises);
196
- const filtered = records.filter(r => r);
197
- debug(`Found ${filtered.length} candidates`);
200
+ const recordPromises = await Promise.allSettled(promises);
201
+ debug(`All recordPromises: ${JSON.stringify(recordPromises)}`);
202
+ const filtered = recordPromises.filter(r => r.status === 'fulfilled').map(r => r.value);
203
+ const failures = recordPromises.filter(r => r.status === 'rejected').map(r => ({
204
+ status: r.reason.status,
205
+ payload: r.reason.payload
206
+ }));
207
+ debug(`Found ${JSON.stringify(recordPromises)} records`);
208
+ debug(`Found ${filtered.length} convertable candidates`);
209
+ debug(`Found ${failures.length} NON-convertable candidates`);
210
+ debug(`Converted: ${JSON.stringify(filtered)}.`);
211
+ debug(`Not converted: ${JSON.stringify(failures)}.`);
198
212
  resolve({
199
213
  nextOffset,
200
214
  records: filtered,
215
+ failures,
201
216
  total: totalRecords
202
217
  });
203
218
  } catch (err) {
219
+ debug(`Error caught on END`);
204
220
  reject(err);
205
221
  }
206
- }).on('record', foundRecord => {
222
+ }).on('record', foundRecordXML => {
207
223
  promises.push(handleRecord()); // eslint-disable-line functional/immutable-data
208
224
 
209
225
  async function handleRecord() {
210
226
  try {
211
- const foundRecordMarc = await _marcRecordSerializers.MARCXML.from(foundRecord, {
227
+ const foundRecordMarc = await _marcRecordSerializers.MARCXML.from(foundRecordXML, {
212
228
  subfieldValues: false
213
229
  });
214
230
  const foundRecordId = getRecordId(foundRecordMarc);
@@ -217,7 +233,15 @@ var _default = ({
217
233
  id: foundRecordId
218
234
  };
219
235
  } catch (err) {
220
- throw new Error(`Failed converting record: ${err}, record: ${foundRecord}`);
236
+ // What should this do?
237
+ const idFromXML = getRecordIdFromXML(foundRecordXML);
238
+ debugData(`Failed converting record: ${err.message}, id: ${idFromXML}, data: ${foundRecordXML}`); //return {message: `Failed converting record: ${err.message}`, id: idFromXML, data: foundRecordXML};
239
+
240
+ throw new _melindaCommons.Error(422, {
241
+ message: `Failed converting record: ${err.message}`,
242
+ id: idFromXML || '000000000',
243
+ data: foundRecordXML
244
+ });
221
245
  }
222
246
  }
223
247
  });
@@ -237,6 +261,12 @@ var _default = ({
237
261
  const [field] = record.get(/^001$/u);
238
262
  return field ? field.value : '';
239
263
  }
264
+
265
+ function getRecordIdFromXML(recordXML) {
266
+ //<controlfield tag=\"001\">015376846</controlfield
267
+ debug(`Cannot yet find possible database record id from recordXML (length ${recordXML.length})`);
268
+ return undefined;
269
+ }
240
270
  };
241
271
 
242
272
  exports.default = _default;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/candidate-search/index.js"],"names":["CandidateSearchError","Error","record","searchSpec","url","maxCandidates","maxRecordsPerRequest","serverMaxResult","MarcRecord","setValidationOptions","subfieldValues","debug","debugData","extend","JSON","stringify","adjustedMaxRecordsPerRequest","inputRecordId","getRecordId","queryList","client","version","retrieveAll","length","queryOffset","resultSetOffset","totalRecords","searchCounter","queryCandidateCounter","queryCounter","maxedQueries","query","records","nextOffset","total","retrieveRecords","newTotalRecords","newQueryCounter","newSearchCounter","newQueryCandidateCounter","maxedQuery","checkMaxedQuery","undefined","newMaxedQueries","concat","queriesLeft","Promise","resolve","reject","promises","searchRetrieve","startRecord","on","err","SruSearchError","all","filtered","filter","r","foundRecord","push","handleRecord","foundRecordMarc","MARCXML","from","foundRecordId","id","field","get","value"],"mappings":"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","sourcesContent":["/**\n*\n* @licstart The following is the entire license notice for the JavaScript code in this file.\n*\n* Melinda record matching modules for Javascript\n*\n* Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)\n*\n* This file is part of melinda-record-matching-js\n*\n* melinda-record-matching-js program is free software: you can redistribute it and/or modify\n* it under the terms of the GNU Lesser General Public License as\n* published by the Free Software Foundation, either version 3 of the\n* License, or (at your option) any later version.\n*\n* melinda-record-matching-js is distributed in the hope that it will be useful,\n* but WITHOUT ANY WARRANTY; without even the implied warranty of\n* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n* GNU Lesser General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License\n* along with this program. If not, see <http://www.gnu.org/licenses/>.\n*\n* @licend The above is the entire license notice\n* for the JavaScript code in this file.\n*\n*/\n\nimport createDebugLogger from 'debug';\nimport createClient, {SruSearchError} from '@natlibfi/sru-client';\nimport {MarcRecord} from '@natlibfi/marc-record';\nimport {MARCXML} from '@natlibfi/marc-record-serializers';\nimport generateQueryList from './query-list';\n\nexport {searchTypes} from './query-list';\n\nexport class CandidateSearchError extends Error {}\n\n// serverMaxResults : maximum size of total search result available from the server, defaults to Aleph's 20000\n\n// eslint-disable-next-line max-statements\nexport default ({record, searchSpec, url, maxCandidates, maxRecordsPerRequest = 50, serverMaxResult = 20000}) => {\n MarcRecord.setValidationOptions({subfieldValues: false});\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search');\n const debugData = debug.extend('data');\n\n debugData(`SearchSpec: ${JSON.stringify(searchSpec)}`);\n debugData(`Url: ${url}`);\n debugData(`MaxRecordsPerRequest ${maxRecordsPerRequest}`);\n debugData(`ServerMaxResult: ${serverMaxResult}`);\n debugData(`MaxCandidates: ${maxCandidates}`);\n\n // Do not retrieve more candidates than defined in maxCandidates\n const adjustedMaxRecordsPerRequest = maxRecordsPerRequest >= maxCandidates ? maxCandidates : maxRecordsPerRequest;\n\n const inputRecordId = getRecordId(record);\n const queryList = generateQueryList(record, searchSpec);\n const client = createClient({\n url,\n maxRecordsPerRequest: adjustedMaxRecordsPerRequest,\n version: '2.0',\n retrieveAll: false\n });\n\n debug(`Searching matches for ${inputRecordId}`);\n debug(`Generated queryList ${JSON.stringify(queryList)}`);\n if (queryList.length === 0) { // eslint-disable-line functional/no-conditional-statement\n throw new CandidateSearchError(`Generated query list contains no queries`);\n }\n\n // state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)\n // state.query : current query (undefined if there was no queries left)\n // state.searchCounter : sequence for current search for current query (undefined, if there we no queries left)\n // 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)\n // state.queriesLeft : amount of queries left\n // state.queryCounter : sequence for current query\n // state.maxedQueries : queries that resulted in more than serverMaxResults hits\n\n\n // eslint-disable-next-line max-statements\n return async ({queryOffset = 0, resultSetOffset = 1, totalRecords = 0, searchCounter = 0, queryCandidateCounter = 0, queryCounter = 0, maxedQueries = []}) => {\n const query = queryList[queryOffset];\n\n if (query) {\n const {records, nextOffset, total} = await retrieveRecords();\n\n // If resultSetOffset === 1 this is the first search for the current query\n debugData(`ResultSetOffset: ${resultSetOffset}`);\n const newTotalRecords = resultSetOffset === 1 ? total : totalRecords;\n const newQueryCounter = resultSetOffset === 1 ? queryCounter + 1 : queryCounter;\n const newSearchCounter = resultSetOffset === 1 ? 1 : searchCounter + 1;\n const newQueryCandidateCounter = resultSetOffset === 1 ? records.length : queryCandidateCounter + records.length;\n\n const maxedQuery = resultSetOffset === 1 ? checkMaxedQuery(query, total, serverMaxResult) : undefined;\n const newMaxedQueries = maxedQuery ? maxedQueries.concat(maxedQuery) : maxedQueries;\n\n if (typeof nextOffset === 'number') {\n debug(`Next search will be for query ${queryOffset} ${query}, starting from record ${nextOffset}`);\n return {records, queryOffset, resultSetOffset: nextOffset, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCounter, maxedQueries: newMaxedQueries};\n }\n debug(`Query ${queryOffset} ${query} done.`);\n debug(`There are (${queryList.length - (queryOffset + 1)} queries left)`);\n return {records, queryOffset: queryOffset + 1, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCounter, maxedQueries: newMaxedQueries};\n }\n\n debug(`All ${queryList.length} queries done, there's no query for ${queryOffset}`);\n return {records: [], queriesLeft: 0, queryCounter, maxedQueries};\n\n function retrieveRecords() {\n return new Promise((resolve, reject) => {\n const promises = [];\n // eslint-disable-next-line functional/no-let\n let totalRecords = 0;\n\n debug(`Searching for candidates with query: ${query} (Offset ${resultSetOffset})`);\n\n client.searchRetrieve(query, {startRecord: resultSetOffset})\n .on('error', err => {\n // eslint-disable-next-line functional/no-conditional-statement\n if (err instanceof SruSearchError) {\n debug(`SRU SruSearchError for query: ${query}: ${err}`);\n reject(new CandidateSearchError(`SRU SruSearchError for query: ${query}: ${err}`));\n }\n debug(`SRU error for query: ${query}: ${err}`);\n reject(new CandidateSearchError(`SRU error for query: ${query}: ${err}`));\n })\n .on('total', total => {\n debug(`Got total: ${total}`);\n totalRecords += total;\n })\n .on('end', async nextOffset => {\n try {\n const records = await Promise.all(promises);\n const filtered = records.filter(r => r);\n\n debug(`Found ${filtered.length} candidates`);\n\n resolve({nextOffset, records: filtered, total: totalRecords});\n } catch (err) {\n reject(err);\n }\n })\n .on('record', foundRecord => {\n promises.push(handleRecord()); // eslint-disable-line functional/immutable-data\n\n async function handleRecord() {\n try {\n const foundRecordMarc = await MARCXML.from(foundRecord, {subfieldValues: false});\n const foundRecordId = getRecordId(foundRecordMarc);\n\n return {record: foundRecordMarc, id: foundRecordId};\n } catch (err) {\n throw new Error(`Failed converting record: ${err}, record: ${foundRecord}`);\n }\n }\n });\n });\n }\n };\n\n function checkMaxedQuery(query, total, serverMaxResult) {\n // eslint-disable-next-line functional/no-conditional-statement\n if (total >= serverMaxResult) {\n debug(`WARNING: Query ${query} resulted in ${total} hits which meets the serverMaxResult (${serverMaxResult}) `);\n return query;\n }\n }\n\n\n function getRecordId(record) {\n const [field] = record.get(/^001$/u);\n return field ? field.value : '';\n }\n};\n"],"file":"index.js"}
1
+ {"version":3,"sources":["../../src/candidate-search/index.js"],"names":["CandidateSearchError","Error","record","searchSpec","url","maxCandidates","maxRecordsPerRequest","serverMaxResult","MarcRecord","setValidationOptions","subfieldValues","debug","debugData","extend","JSON","stringify","adjustedMaxRecordsPerRequest","inputRecordId","getRecordId","queryList","client","version","retrieveAll","length","queryOffset","resultSetOffset","totalRecords","searchCounter","queryCandidateCounter","queryCounter","maxedQueries","query","records","failures","nextOffset","total","retrieveRecords","newTotalRecords","newQueryCounter","newSearchCounter","newQueryCandidateCounter","maxedQuery","checkMaxedQuery","undefined","newMaxedQueries","concat","queriesLeft","Promise","resolve","reject","promises","searchRetrieve","startRecord","on","err","SruSearchError","recordPromises","allSettled","filtered","filter","r","status","map","value","reason","payload","foundRecordXML","push","handleRecord","foundRecordMarc","MARCXML","from","foundRecordId","id","idFromXML","getRecordIdFromXML","message","MatchingError","data","field","get","recordXML"],"mappings":"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","sourcesContent":["/**\n*\n* @licstart The following is the entire license notice for the JavaScript code in this file.\n*\n* Melinda record matching modules for Javascript\n*\n* Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)\n*\n* This file is part of melinda-record-matching-js\n*\n* melinda-record-matching-js program is free software: you can redistribute it and/or modify\n* it under the terms of the GNU Lesser General Public License as\n* published by the Free Software Foundation, either version 3 of the\n* License, or (at your option) any later version.\n*\n* melinda-record-matching-js is distributed in the hope that it will be useful,\n* but WITHOUT ANY WARRANTY; without even the implied warranty of\n* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n* GNU Lesser General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License\n* along with this program. If not, see <http://www.gnu.org/licenses/>.\n*\n* @licend The above is the entire license notice\n* for the JavaScript code in this file.\n*\n*/\n\nimport createDebugLogger from 'debug';\nimport createClient, {SruSearchError} from '@natlibfi/sru-client';\nimport {MarcRecord} from '@natlibfi/marc-record';\nimport {MARCXML} from '@natlibfi/marc-record-serializers';\nimport generateQueryList from './query-list';\nimport {Error as MatchingError} from '@natlibfi/melinda-commons';\n\nexport {searchTypes} from './query-list';\n\nexport class CandidateSearchError extends Error {}\n\n// serverMaxResults : maximum size of total search result available from the server, defaults to Aleph's 20000\n\n// eslint-disable-next-line max-statements\nexport default ({record, searchSpec, url, maxCandidates, maxRecordsPerRequest = 50, serverMaxResult = 20000}) => {\n MarcRecord.setValidationOptions({subfieldValues: false});\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search');\n const debugData = debug.extend('data');\n\n debugData(`SearchSpec: ${JSON.stringify(searchSpec)}`);\n debugData(`Url: ${url}`);\n debugData(`MaxRecordsPerRequest ${maxRecordsPerRequest}`);\n debugData(`ServerMaxResult: ${serverMaxResult}`);\n debugData(`MaxCandidates: ${maxCandidates}`);\n\n // Do not retrieve more candidates than defined in maxCandidates\n const adjustedMaxRecordsPerRequest = maxRecordsPerRequest >= maxCandidates ? maxCandidates : maxRecordsPerRequest;\n\n const inputRecordId = getRecordId(record);\n const queryList = generateQueryList(record, searchSpec);\n const client = createClient({\n url,\n maxRecordsPerRequest: adjustedMaxRecordsPerRequest,\n version: '2.0',\n retrieveAll: false\n });\n\n debug(`Searching matches for ${inputRecordId}`);\n debug(`Generated queryList ${JSON.stringify(queryList)}`);\n if (queryList.length === 0) { // eslint-disable-line functional/no-conditional-statement\n throw new CandidateSearchError(`Generated query list contains no queries`);\n }\n\n // state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)\n // state.query : current query (undefined if there was no queries left)\n // state.searchCounter : sequence for current search for current query (undefined, if there we no queries left)\n // 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)\n // state.queriesLeft : amount of queries left\n // state.queryCounter : sequence for current query\n // state.maxedQueries : queries that resulted in more than serverMaxResults hits\n\n\n return async ({queryOffset = 0, resultSetOffset = 1, totalRecords = 0, searchCounter = 0, queryCandidateCounter = 0, queryCounter = 0, maxedQueries = []}) => {\n const query = queryList[queryOffset];\n\n if (query) {\n const {records, failures, nextOffset, total} = await retrieveRecords();\n\n // If resultSetOffset === 1 this is the first search for the current query\n debugData(`ResultSetOffset: ${resultSetOffset}`);\n const newTotalRecords = resultSetOffset === 1 ? total : totalRecords;\n const newQueryCounter = resultSetOffset === 1 ? queryCounter + 1 : queryCounter;\n const newSearchCounter = resultSetOffset === 1 ? 1 : searchCounter + 1;\n const newQueryCandidateCounter = resultSetOffset === 1 ? records.length + failures.length : queryCandidateCounter + records.length + failures.length;\n\n const maxedQuery = resultSetOffset === 1 ? checkMaxedQuery(query, total, serverMaxResult) : undefined;\n const newMaxedQueries = maxedQuery ? maxedQueries.concat(maxedQuery) : maxedQueries;\n\n if (typeof nextOffset === 'number') {\n debug(`Next search will be for query ${queryOffset} ${query}, starting from record ${nextOffset}`);\n return {records, failures, queryOffset, resultSetOffset: nextOffset, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCounter, maxedQueries: newMaxedQueries};\n }\n debug(`Query ${queryOffset} ${query} done.`);\n debug(`There are (${queryList.length - (queryOffset + 1)} queries left)`);\n return {records, failures, queryOffset: queryOffset + 1, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCounter, maxedQueries: newMaxedQueries};\n }\n\n debug(`All ${queryList.length} queries done, there's no query for ${queryOffset}`);\n return {records: [], failures: [], queriesLeft: 0, queryCounter, maxedQueries};\n\n function retrieveRecords() {\n return new Promise((resolve, reject) => {\n const promises = [];\n // eslint-disable-next-line functional/no-let\n let totalRecords = 0;\n\n debug(`Searching for candidates with query: ${query} (Offset ${resultSetOffset})`);\n\n client.searchRetrieve(query, {startRecord: resultSetOffset})\n .on('error', err => {\n // eslint-disable-next-line functional/no-conditional-statement\n if (err instanceof SruSearchError) {\n debug(`SRU SruSearchError for query: ${query}: ${err}`);\n reject(new CandidateSearchError(`SRU SruSearchError for query: ${query}: ${err}`));\n }\n debug(`SRU error for query: ${query}: ${err}`);\n reject(new CandidateSearchError(`SRU error for query: ${query}: ${err}`));\n })\n .on('total', total => {\n debug(`Got total: ${total}`);\n totalRecords += total;\n })\n .on('end', async nextOffset => {\n try {\n const recordPromises = await Promise.allSettled(promises);\n debug(`All recordPromises: ${JSON.stringify(recordPromises)}`);\n const filtered = recordPromises.filter(r => r.status === 'fulfilled').map(r => r.value);\n const failures = recordPromises.filter(r => r.status === 'rejected').map(r => ({status: r.reason.status, payload: r.reason.payload}));\n\n debug(`Found ${JSON.stringify(recordPromises)} records`);\n debug(`Found ${filtered.length} convertable candidates`);\n debug(`Found ${failures.length} NON-convertable candidates`);\n debug(`Converted: ${JSON.stringify(filtered)}.`);\n debug(`Not converted: ${JSON.stringify(failures)}.`);\n\n\n resolve({nextOffset, records: filtered, failures, total: totalRecords});\n } catch (err) {\n debug(`Error caught on END`);\n reject(err);\n }\n })\n .on('record', foundRecordXML => {\n promises.push(handleRecord()); // eslint-disable-line functional/immutable-data\n\n async function handleRecord() {\n try {\n const foundRecordMarc = await MARCXML.from(foundRecordXML, {subfieldValues: false});\n const foundRecordId = getRecordId(foundRecordMarc);\n\n return {record: foundRecordMarc, id: foundRecordId};\n } catch (err) {\n // What should this do?\n const idFromXML = getRecordIdFromXML(foundRecordXML);\n debugData(`Failed converting record: ${err.message}, id: ${idFromXML}, data: ${foundRecordXML}`);\n //return {message: `Failed converting record: ${err.message}`, id: idFromXML, data: foundRecordXML};\n throw new MatchingError(422, {message: `Failed converting record: ${err.message}`, id: idFromXML || '000000000', data: foundRecordXML});\n }\n }\n });\n });\n }\n };\n\n function checkMaxedQuery(query, total, serverMaxResult) {\n // eslint-disable-next-line functional/no-conditional-statement\n if (total >= serverMaxResult) {\n debug(`WARNING: Query ${query} resulted in ${total} hits which meets the serverMaxResult (${serverMaxResult}) `);\n return query;\n }\n }\n\n\n function getRecordId(record) {\n const [field] = record.get(/^001$/u);\n return field ? field.value : '';\n }\n\n function getRecordIdFromXML(recordXML) {\n //<controlfield tag=\\\"001\\\">015376846</controlfield\n debug(`Cannot yet find possible database record id from recordXML (length ${recordXML.length})`);\n return undefined;\n }\n\n};\n"],"file":"index.js"}
@@ -8,6 +8,8 @@ var _fixugenHttpClient = _interopRequireDefault(require("@natlibfi/fixugen-http-
8
8
 
9
9
  var _marcRecord = require("@natlibfi/marc-record");
10
10
 
11
+ var _melindaCommons = require("@natlibfi/melinda-commons");
12
+
11
13
  var _ = _interopRequireWildcard(require("."));
12
14
 
13
15
  var _debug = _interopRequireDefault(require("debug"));
@@ -107,6 +109,7 @@ describe('candidate-search', () => {
107
109
  async function iterate({
108
110
  searchOptions,
109
111
  expectedSearchError,
112
+ expectedErrorStatus,
110
113
  count = 1
111
114
  }) {
112
115
  const expectedResults = getFixture(`expectedResults${count}.json`);
@@ -117,8 +120,18 @@ describe('candidate-search', () => {
117
120
  await search(searchOptions);
118
121
  throw new Error('Expected an error');
119
122
  } catch (err) {
123
+ debug(`Got an error: ${err}`);
120
124
  (0, _chai.expect)(err).to.be.an('error');
121
- (0, _chai.expect)(err.message).to.match(new RegExp(expectedSearchError, 'u'));
125
+ const errorMessage = err instanceof _melindaCommons.Error ? err.payload.message : err.message;
126
+ const errorStatus = err instanceof _melindaCommons.Error ? err.status : undefined;
127
+ debug(`errorMessage: ${errorMessage}, errorStatus: ${errorStatus}`);
128
+ (0, _chai.expect)(errorMessage).to.match(new RegExp(expectedSearchError, 'u'));
129
+
130
+ if (expectedErrorStatus) {
131
+ (0, _chai.expect)(errorStatus).to.be(expectedErrorStatus);
132
+ return;
133
+ }
134
+
122
135
  return;
123
136
  }
124
137
  } // eslint-disable-next-line functional/no-conditional-statement
@@ -130,7 +143,8 @@ describe('candidate-search', () => {
130
143
  }
131
144
 
132
145
  function formatResults(results) {
133
- // console.log(results); //eslint-disable-line
146
+ debug(results); //eslint-disable-line
147
+
134
148
  return { ...results,
135
149
  records: results.records.map(({
136
150
  record,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/candidate-search/index.spec.js"],"names":["debug","describe","callback","path","__dirname","recurse","fixura","reader","READERS","JSON","getFixture","factoryOptions","searchOptions","expectedFactoryError","expectedSearchError","enabled","url","isCandidateSearchError","formatFactoryOptions","to","throw","CandidateSearchError","RegExp","search","iterate","stringify","maxRecordsPerRequest","maxServerResults","undefined","record","MarcRecord","subfieldValues","count","expectedResults","Error","err","be","an","message","match","results","formatResults","eql","records","map","id","toObject"],"mappings":"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","sourcesContent":["/**\n*\n* @licstart The following is the entire license notice for the JavaScript code in this file.\n*\n* Melinda record matching modules for Javascript\n*\n* Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)\n*\n* This file is part of melinda-record-matching-js\n*\n* melinda-record-matching-js program is free software: you can redistribute it and/or modify\n* it under the terms of the GNU Lesser General Public License as\n* published by the Free Software Foundation, either version 3 of the\n* License, or (at your option) any later version.\n*\n* melinda-record-matching-js is distributed in the hope that it will be useful,\n* but WITHOUT ANY WARRANTY; without even the implied warranty of\n* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n* GNU Lesser General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License\n* along with this program. If not, see <http://www.gnu.org/licenses/>.\n*\n* @licend The above is the entire license notice\n* for the JavaScript code in this file.\n*\n*/\n\nimport {expect} from 'chai';\nimport {READERS} from '@natlibfi/fixura';\nimport generateTests from '@natlibfi/fixugen-http-client';\nimport {MarcRecord} from '@natlibfi/marc-record';\nimport createSearchInterface, {CandidateSearchError} from '.';\nimport createDebugLogger from 'debug';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:test');\n\ndescribe('candidate-search', () => {\n generateTests({\n callback,\n path: [__dirname, '..', '..', 'test-fixtures', 'candidate-search', 'index'],\n recurse: false,\n fixura: {\n reader: READERS.JSON\n }\n });\n\n // eslint-disable-next-line max-statements\n async function callback({getFixture, factoryOptions, searchOptions, expectedFactoryError = false, expectedSearchError = false, enabled = true}) {\n const url = 'http://foo.bar';\n\n if (!enabled) {\n return;\n }\n\n if (expectedFactoryError) {\n if (expectedFactoryError.isCandidateSearchError) {\n expect(() => createSearchInterface({...formatFactoryOptions(), url})).to.throw(CandidateSearchError, new RegExp(expectedFactoryError, 'u'));\n return;\n }\n\n expect(() => createSearchInterface({...formatFactoryOptions(), url})).to.throw(new RegExp(expectedFactoryError, 'u'));\n return;\n }\n\n const search = createSearchInterface({...formatFactoryOptions(), url});\n await iterate({searchOptions, expectedSearchError});\n\n function formatFactoryOptions() {\n debug(`Using factoryOptions: ${JSON.stringify(factoryOptions)}`);\n return {\n ...factoryOptions,\n maxRecordsPerRequest: factoryOptions.maxRecordsPerRequest || 1,\n maxServerResults: factoryOptions.maxServerResults || undefined,\n record: new MarcRecord(factoryOptions.record, {subfieldValues: false})\n };\n }\n\n // eslint-disable-next-line max-statements\n async function iterate({searchOptions, expectedSearchError, count = 1}) {\n const expectedResults = getFixture(`expectedResults${count}.json`);\n\n if (expectedSearchError) { // eslint-disable-line functional/no-conditional-statement\n try {\n await search(searchOptions);\n throw new Error('Expected an error');\n } catch (err) {\n expect(err).to.be.an('error');\n expect(err.message).to.match(new RegExp(expectedSearchError, 'u'));\n return;\n }\n }\n\n // eslint-disable-next-line functional/no-conditional-statement\n if (!expectedSearchError) {\n const results = await search(searchOptions);\n expect(formatResults(results)).to.eql(expectedResults);\n }\n\n function formatResults(results) {\n // console.log(results); //eslint-disable-line\n return {\n ...results,\n records: results.records.map(({record, id}) => ({id, record: record.toObject()}))\n };\n }\n\n }\n }\n});\n"],"file":"index.spec.js"}
1
+ {"version":3,"sources":["../../src/candidate-search/index.spec.js"],"names":["debug","describe","callback","path","__dirname","recurse","fixura","reader","READERS","JSON","getFixture","factoryOptions","searchOptions","expectedFactoryError","expectedSearchError","enabled","url","isCandidateSearchError","formatFactoryOptions","to","throw","CandidateSearchError","RegExp","search","iterate","stringify","maxRecordsPerRequest","maxServerResults","undefined","record","MarcRecord","subfieldValues","expectedErrorStatus","count","expectedResults","Error","err","be","an","errorMessage","MatchingError","payload","message","errorStatus","status","match","results","formatResults","eql","records","map","id","toObject"],"mappings":"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","sourcesContent":["/**\n*\n* @licstart The following is the entire license notice for the JavaScript code in this file.\n*\n* Melinda record matching modules for Javascript\n*\n* Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)\n*\n* This file is part of melinda-record-matching-js\n*\n* melinda-record-matching-js program is free software: you can redistribute it and/or modify\n* it under the terms of the GNU Lesser General Public License as\n* published by the Free Software Foundation, either version 3 of the\n* License, or (at your option) any later version.\n*\n* melinda-record-matching-js is distributed in the hope that it will be useful,\n* but WITHOUT ANY WARRANTY; without even the implied warranty of\n* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n* GNU Lesser General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License\n* along with this program. If not, see <http://www.gnu.org/licenses/>.\n*\n* @licend The above is the entire license notice\n* for the JavaScript code in this file.\n*\n*/\n\nimport {expect} from 'chai';\nimport {READERS} from '@natlibfi/fixura';\nimport generateTests from '@natlibfi/fixugen-http-client';\nimport {MarcRecord} from '@natlibfi/marc-record';\nimport {Error as MatchingError} from '@natlibfi/melinda-commons';\nimport createSearchInterface, {CandidateSearchError} from '.';\nimport createDebugLogger from 'debug';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:test');\n\ndescribe('candidate-search', () => {\n generateTests({\n callback,\n path: [__dirname, '..', '..', 'test-fixtures', 'candidate-search', 'index'],\n recurse: false,\n fixura: {\n reader: READERS.JSON\n }\n });\n\n // eslint-disable-next-line max-statements\n async function callback({getFixture, factoryOptions, searchOptions, expectedFactoryError = false, expectedSearchError = false, enabled = true}) {\n const url = 'http://foo.bar';\n\n if (!enabled) {\n return;\n }\n\n if (expectedFactoryError) {\n if (expectedFactoryError.isCandidateSearchError) {\n expect(() => createSearchInterface({...formatFactoryOptions(), url})).to.throw(CandidateSearchError, new RegExp(expectedFactoryError, 'u'));\n return;\n }\n\n expect(() => createSearchInterface({...formatFactoryOptions(), url})).to.throw(new RegExp(expectedFactoryError, 'u'));\n return;\n }\n\n const search = createSearchInterface({...formatFactoryOptions(), url});\n await iterate({searchOptions, expectedSearchError});\n\n function formatFactoryOptions() {\n debug(`Using factoryOptions: ${JSON.stringify(factoryOptions)}`);\n return {\n ...factoryOptions,\n maxRecordsPerRequest: factoryOptions.maxRecordsPerRequest || 1,\n maxServerResults: factoryOptions.maxServerResults || undefined,\n record: new MarcRecord(factoryOptions.record, {subfieldValues: false})\n };\n }\n\n // eslint-disable-next-line max-statements\n async function iterate({searchOptions, expectedSearchError, expectedErrorStatus, count = 1}) {\n const expectedResults = getFixture(`expectedResults${count}.json`);\n\n if (expectedSearchError) { // eslint-disable-line functional/no-conditional-statement\n try {\n await search(searchOptions);\n throw new Error('Expected an error');\n } catch (err) {\n debug(`Got an error: ${err}`);\n expect(err).to.be.an('error');\n const errorMessage = err instanceof MatchingError ? err.payload.message : err.message;\n const errorStatus = err instanceof MatchingError ? err.status : undefined;\n debug(`errorMessage: ${errorMessage}, errorStatus: ${errorStatus}`);\n expect(errorMessage).to.match(new RegExp(expectedSearchError, 'u'));\n\n if (expectedErrorStatus) {\n expect(errorStatus).to.be(expectedErrorStatus);\n return;\n }\n return;\n }\n }\n\n // eslint-disable-next-line functional/no-conditional-statement\n if (!expectedSearchError) {\n const results = await search(searchOptions);\n expect(formatResults(results)).to.eql(expectedResults);\n }\n\n function formatResults(results) {\n debug(results); //eslint-disable-line\n return {\n ...results,\n records: results.records.map(({record, id}) => ({id, record: record.toObject()}))\n };\n }\n\n }\n }\n});\n"],"file":"index.spec.js"}
package/dist/index.js CHANGED
@@ -55,7 +55,8 @@ var _default = ({
55
55
  maxCandidates = 25,
56
56
  returnStrategy = false,
57
57
  returnQuery = false,
58
- returnNonMatches = false
58
+ returnNonMatches = false,
59
+ returnFailures
59
60
  }) => {
60
61
  const debug = (0, _debug.default)('@natlibfi/melinda-record-matching:index');
61
62
  const debugData = debug.extend('data');
@@ -66,6 +67,7 @@ var _default = ({
66
67
  debugData(`ReturnStrategy: ${JSON.stringify(returnStrategy)}`);
67
68
  debugData(`ReturnQuery: ${JSON.stringify(returnQuery)}`);
68
69
  debugData(`ReturnNonMatches: ${JSON.stringify(returnNonMatches)}`);
70
+ debugData(`ReturnFailures: ${JSON.stringify(returnFailures)}`);
69
71
  const detect = (0, matchDetection.default)(detectionOptions, returnStrategy);
70
72
  return record => {
71
73
  const search = (0, candidateSearch.default)({ ...searchOptions,
@@ -90,16 +92,21 @@ var _default = ({
90
92
  candidateCount = 0,
91
93
  nonMatches = [],
92
94
  duplicateCount = 0,
93
- nonMatchCount = 0
95
+ nonMatchCount = 0,
96
+ conversionFailures = []
94
97
  }) {
95
98
  debugData(`Starting next matcher iteration.`);
96
99
  const {
97
100
  records,
101
+ failures,
98
102
  ...state
99
103
  } = await search(initialState);
100
- debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}`);
104
+ debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, nonMatchCount: ${nonMatchCount}, conversionFailures: ${conversionFailures}`);
101
105
  const recordSetSize = records.length;
102
- const newCandidateCount = candidateCount + recordSetSize;
106
+ const failureSetSize = failures.length;
107
+ const newCandidateCount = candidateCount + recordSetSize + failureSetSize;
108
+ const newConversionFailures = conversionFailures.concat(failures);
109
+ debugData(`Failures: ${failures.length}, ConversionFailures: ${conversionFailures.length}, NewConversionFailures: ${newConversionFailures.length}`);
103
110
 
104
111
  if (recordSetSize > 0) {
105
112
  return handleRecordSet();
@@ -112,7 +119,9 @@ var _default = ({
112
119
  matches,
113
120
  candidateCount: newCandidateCount,
114
121
  nonMatches,
115
- duplicateCount
122
+ nonMatchCount,
123
+ duplicateCount,
124
+ conversionFailures: newConversionFailures
116
125
  });
117
126
  }
118
127
 
@@ -122,8 +131,10 @@ var _default = ({
122
131
  state,
123
132
  stopReason: '',
124
133
  nonMatches,
134
+ nonMatchCount,
125
135
  candidateCount: newCandidateCount,
126
- duplicateCount
136
+ duplicateCount,
137
+ conversionFailures: newConversionFailures
127
138
  });
128
139
 
129
140
  function handleRecordSet() {
@@ -133,7 +144,8 @@ var _default = ({
133
144
  recordSetSize,
134
145
  maxMatches,
135
146
  matches,
136
- nonMatches
147
+ nonMatches,
148
+ nonMatchCount
137
149
  });
138
150
  const newDuplicateCount = duplicateCount + matchResult.duplicateCount;
139
151
  const newNonMatchCount = nonMatchCount + matchResult.nonMatchCount;
@@ -153,7 +165,8 @@ var _default = ({
153
165
  nonMatches: newNonMatches,
154
166
  duplicateCount: newDuplicateCount,
155
167
  candidateCount: newCandidateCount,
156
- nonMatchCount: newNonMatchCount
168
+ nonMatchCount: newNonMatchCount,
169
+ conversionFailures: newConversionFailures
157
170
  });
158
171
  }
159
172
 
@@ -165,7 +178,8 @@ var _default = ({
165
178
  nonMatches: newNonMatches,
166
179
  duplicateCount: newDuplicateCount,
167
180
  candidateCount: newCandidateCount,
168
- nonMatchCount: newNonMatchCount
181
+ nonMatchCount: newNonMatchCount,
182
+ conversionFailures: newConversionFailures
169
183
  });
170
184
  }
171
185
 
@@ -175,7 +189,8 @@ var _default = ({
175
189
  candidateCount: newCandidateCount,
176
190
  nonMatches: newNonMatches,
177
191
  duplicateCount: newDuplicateCount,
178
- nonMatchCount: newNonMatchCount
192
+ nonMatchCount: newNonMatchCount,
193
+ conversionFailures: newConversionFailures
179
194
  });
180
195
  }
181
196
 
@@ -225,7 +240,7 @@ var _default = ({
225
240
  // - matchQuery (if returnQuery option is true)
226
241
  // we could have here also returnRecords/returnMatchRecords/returnNonMatchRecord options that could be turned false for not to return actual record data
227
242
  // matchStatus.status: boolean, true if matcher retrieved and handled all found candidate records, false if it did not
228
- // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries',empty string/undefined), reason for stopping retrieving or handling the candidate records
243
+ // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries','conversionFailures', empty string/undefined), reason for stopping retrieving or handling the candidate records
229
244
  // - only one stopReason is returned (if there would be several possible stopReasons, stopReason is picked in the above order)
230
245
  // - 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
231
246
 
@@ -237,18 +252,21 @@ var _default = ({
237
252
  nonMatches,
238
253
  duplicateCount,
239
254
  candidateCount,
240
- nonMatchCount
255
+ nonMatchCount,
256
+ conversionFailures
241
257
  }) {
258
+ const conversionFailureCount = conversionFailures.length;
242
259
  checkCounts({
243
260
  matches,
244
261
  nonMatches,
245
262
  candidateCount,
246
263
  duplicateCount,
247
- nonMatchCount
264
+ nonMatchCount,
265
+ conversionFailureCount
248
266
  });
249
- const matchStatus = getMatchState(state, stopReason); // add nonMatches to result only if returnNonMatches is 'true', otherwise nonMatches have not been gathered
267
+ const matchStatus = getMatchState(state, stopReason, conversionFailureCount); // add nonMatches to result only if returnNonMatches is 'true', otherwise nonMatches have not been gathered
250
268
 
251
- const result = returnNonMatches ? {
269
+ const matchesResult = returnNonMatches ? {
252
270
  matches,
253
271
  matchStatus,
254
272
  nonMatches
@@ -256,6 +274,10 @@ var _default = ({
256
274
  matches,
257
275
  matchStatus
258
276
  };
277
+ const result = returnFailures ? { ...matchesResult,
278
+ conversionFailures
279
+ } : matchesResult;
280
+ debugData(`ReturnFailures ${returnFailures}`);
259
281
  debugData(`${JSON.stringify(result)}`);
260
282
  return result; // note that in cases where the matching has been stopped because of maxMatches checkCounts won't (in most cases) match
261
283
 
@@ -264,13 +286,14 @@ var _default = ({
264
286
  nonMatches,
265
287
  candidateCount,
266
288
  duplicateCount,
267
- nonMatchCount
289
+ nonMatchCount,
290
+ conversionFailureCount
268
291
  }) {
269
292
  const matchCount = matches.length;
270
293
  debugData(`Return nonMatches: ${returnNonMatches}`);
271
294
  const chosenNonMatchCount = returnNonMatches ? nonMatches.length : nonMatchCount;
272
- const totalHandled = matchCount + nonMatchCount + duplicateCount;
273
- debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}`);
295
+ const totalHandled = matchCount + chosenNonMatchCount + duplicateCount;
296
+ debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}, conversionFailureCount: ${conversionFailureCount}`);
274
297
  debug(`We got result for ${totalHandled} / ${candidateCount} retrieved candidates`);
275
298
 
276
299
  if (totalHandled !== candidateCount) {
@@ -281,18 +304,22 @@ var _default = ({
281
304
  return;
282
305
  }
283
306
 
284
- function getMatchState(state, stopReason) {
307
+ function getMatchState(state, stopReason, conversionFailuresCount) {
285
308
  debugData(`${JSON.stringify(state)}`);
309
+ debug(`We had ${conversionFailuresCount} retrieved candidates that could not be converted.`);
286
310
  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}`);
287
311
  debugData(`StopReason: <${stopReason}>`);
288
312
  const searchesLeft = state.resultSetOffset && state.resultSetOffset <= state.totalRecords;
289
313
  const nonRetrieved = searchesLeft ? state.totalRecords - state.queryCandidateCounter : 0;
290
- debugData(`nonRetrieved: ${nonRetrieved}`);
314
+ debugData(`nonRetrieved: ${nonRetrieved}`); // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries','conversionFailures', empty string/undefined), reason for stopping retrieving or handling the candidate records
315
+ // 'maxMatches' and 'maxCandidates' are in stopReason, 'maxedQueries' and 'conversionFailures' are created here
291
316
 
292
- if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0) {
293
- const maxedQueriesStopReason = state.maxedQueries.length > 0 ? 'maxedQueries' : '';
294
- const newStopReason = stopReason === '' ? maxedQueriesStopReason : stopReason;
317
+ if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0 || conversionFailureCount > 0) {
318
+ const maxedQueriesStopReason = state.maxedQueries.length > 0 ? 'maxedQueries' : undefined;
319
+ const conversionFailuresStopReason = conversionFailureCount > 0 ? 'conversionFailures' : undefined;
320
+ const newStopReason = stopReason === '' || stopReason === undefined ? maxedQueriesStopReason || conversionFailuresStopReason : stopReason;
295
321
  debugData(`MaxedQueriesStopReason: <${maxedQueriesStopReason}>`);
322
+ debugData(`ConversionFailureStopReason <${conversionFailuresStopReason}>`);
296
323
  debugData(`NewStopReason: <${newStopReason}>`);
297
324
  debug(`Match status: false`);
298
325
  return {
@@ -411,12 +438,15 @@ var _default = ({
411
438
  function handleRecordMatch(isMatch, newMatch) {
412
439
  const newRecordMatches = isMatch ? recordMatches.concat(newMatch) : recordMatches;
413
440
  const newRecordNonMatches = isMatch ? recordNonMatches : recordNonMatches.concat(newMatch);
441
+ const newRecordNonMatchCount = isMatch ? recordNonMatchCount : recordNonMatchCount + 1;
414
442
  debugData(`- Total matches after this detection: ${matches.concat(newRecordMatches).length} (max: ${maxMatches})`); // eslint-disable-next-line functional/no-conditional-statement
415
443
 
416
444
  if (returnNonMatches) {
417
445
  debugData(`- Total nonMatches after this detection: ${nonMatches.concat(newRecordNonMatches).length}`);
418
446
  }
419
447
 
448
+ debugData(`- Total nonMatchCount after this detection: ${recordNonMatchCount}`);
449
+
420
450
  if (maxMatchesFound({
421
451
  matches: matches.concat(newRecordMatches),
422
452
  maxMatches
@@ -426,7 +456,7 @@ var _default = ({
426
456
  matches: newRecordMatches,
427
457
  nonMatches: returnNonMatches ? newRecordNonMatches : [],
428
458
  duplicateCount: recordDuplicateCount,
429
- nonMatchCount: recordNonMatchCount
459
+ nonMatchCount: newRecordNonMatchCount
430
460
  };
431
461
  }
432
462
 
@@ -439,7 +469,7 @@ var _default = ({
439
469
  recordCount: newRecordCount,
440
470
  recordNonMatches: returnNonMatches ? newRecordNonMatches : [],
441
471
  duplicateCount: recordDuplicateCount,
442
- recordNonMatchCount
472
+ recordNonMatchCount: newRecordNonMatchCount
443
473
  });
444
474
  }
445
475
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.js"],"names":["detection","detectionOptions","search","searchOptions","maxMatches","maxCandidates","returnStrategy","returnQuery","returnNonMatches","debug","debugData","extend","JSON","stringify","detect","record","iterate","initialState","matches","candidateCount","nonMatches","duplicateCount","nonMatchCount","records","state","length","recordSetSize","newCandidateCount","handleRecordSet","queriesLeft","searchCounter","query","returnResult","stopReason","matchResult","iterateRecords","newDuplicateCount","newNonMatchCount","newMatches","newNonMatches","handleMatchResult","maxMatchesFound","maxCandidatesRetrieved","concat","addQuery","map","match","matchQuery","checkCounts","matchStatus","getMatchState","result","matchCount","chosenNonMatchCount","totalHandled","resultSetOffset","totalRecords","queryCandidateCounter","maxedQueries","searchesLeft","nonRetrieved","maxedQueriesStopReason","newStopReason","status","recordMatches","recordNonMatches","recordCount","recordDuplicateCount","recordNonMatchCount","candidate","newRecordCount","candidateNotInMatches","candidateRecord","id","candidateId","detectionResult","handleDetectionResult","slice","strategy","treshold","probability","strategyResult","newMatch","handleRecordMatch","newRecordNonMatchCount","isMatch","newRecordMatches","newRecordNonMatches","newCandidateId","find"],"mappings":"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","sourcesContent":["/**\n*\n* @licstart The following is the entire license notice for the JavaScript code in this file.\n*\n* Melinda record matching modules for Javascript\n*\n* Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)\n*\n* This file is part of melinda-record-matching-js\n*\n* melinda-record-matching-js program is free software: you can redistribute it and/or modify\n* it under the terms of the GNU Lesser General Public License as\n* published by the Free Software Foundation, either version 3 of the\n* License, or (at your option) any later version.\n*\n* melinda-record-matching-js is distributed in the hope that it will be useful,\n* but WITHOUT ANY WARRANTY; without even the implied warranty of\n* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n* GNU Lesser General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License\n* along with this program. If not, see <http://www.gnu.org/licenses/>.\n*\n* @licend The above is the entire license notice\n* for the JavaScript code in this file.\n*\n*/\n\nimport createDebugLogger from 'debug';\nimport createSearchInterface, * as candidateSearch from './candidate-search';\nimport createDetectionInterface, * as matchDetection from './match-detection';\n\nexport {candidateSearch, matchDetection};\n\nexport default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false}) => {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:index');\n const debugData = debug.extend('data');\n\n debugData(`DetectionOptions: ${JSON.stringify(detectionOptions)}`);\n debugData(`SearchOptions: ${JSON.stringify(searchOptions)}`);\n debugData(`MaxMatches: ${JSON.stringify(maxMatches)}`);\n debugData(`MaxCandidates: ${JSON.stringify(maxCandidates)}`);\n debugData(`ReturnStrategy: ${JSON.stringify(returnStrategy)}`);\n debugData(`ReturnQuery: ${JSON.stringify(returnQuery)}`);\n debugData(`ReturnNonMatches: ${JSON.stringify(returnNonMatches)}`);\n\n const detect = createDetectionInterface(detectionOptions, returnStrategy);\n\n return record => {\n const search = createSearchInterface({...searchOptions, record, maxCandidates});\n return iterate({});\n\n // candidateCount : amount of candidate records retrived from SRU for matching, NOT including current record set\n // matches : candidates that have been detected as matches by current matcher job\n // nonMatches : candidates that have been detected as non-matches by current matcher job (only if returnNonMatches is 'true')\n // duplicateCount : amount of candidate records that were retrieved from the SRU but not handled further because they were already found in the matches/nonMatches\n\n // state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)\n // state.query : current query (undefined if there was no queries left)\n // state.searchCounter : sequence for current search for current query (undefined, if there we no queries left)\n // 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)\n // state.queriesLeft : amount of queries left\n // state.queryCounter : sequence for current query\n // state.maxedQueries : queries that resulted in more than serverMaxResults hits\n\n async function iterate({initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0, nonMatchCount = 0}) {\n debugData(`Starting next matcher iteration.`);\n const {records, ...state} = await search(initialState);\n\n debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}`);\n const recordSetSize = records.length;\n const newCandidateCount = candidateCount + recordSetSize;\n\n if (recordSetSize > 0) {\n return handleRecordSet();\n }\n\n if (state.queriesLeft > 0) {\n debug(`Empty record set ${state.searchCounter} for ${state.query}, but there are ${state.queriesLeft} queries left`);\n return iterate({initialState: state, matches, candidateCount: newCandidateCount, nonMatches, duplicateCount});\n }\n\n debug(`No (more) candidate records to check, no more queries left, matches: ${matches.length}`);\n return returnResult({matches, state, stopReason: '', nonMatches, candidateCount: newCandidateCount, duplicateCount});\n\n function handleRecordSet() {\n debug(`Checking record set of ${recordSetSize} candidate records for possible matches, found by ${state.searchCounter} search for ${state.query}`);\n\n const matchResult = iterateRecords({records, recordSetSize, maxMatches, matches, nonMatches});\n const newDuplicateCount = duplicateCount + matchResult.duplicateCount;\n const newNonMatchCount = nonMatchCount + matchResult.nonMatchCount;\n const {newMatches, newNonMatches} = handleMatchResult(matchResult, matches, nonMatches);\n\n if (maxMatchesFound({matches: newMatches, maxMatches})) {\n return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount});\n }\n\n if (maxCandidatesRetrieved(newCandidateCount, maxCandidates)) {\n return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount});\n }\n\n return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount});\n }\n\n function handleMatchResult(matchResult, matches, nonMatches) {\n debugData(`- Amount of new matches from record set: ${matchResult.matches.length}`);\n // eslint-disable-next-line functional/no-conditional-statement\n if (returnNonMatches) {\n debugData(`- Amount of new nonMatches from record set: ${matchResult.nonMatches.length}`);\n }\n\n const newMatches = matches.concat(returnQuery ? addQuery(matchResult.matches) : matchResult.matches);\n const newNonMatches = returnNonMatches ? nonMatches.concat(returnQuery ? addQuery(matchResult.nonMatches) : matchResult.nonMatches) : [];\n\n debugData(`- Total amount of matches: ${newMatches.length}`);\n // eslint-disable-next-line functional/no-conditional-statement\n if (returnNonMatches) {\n debugData(`- Total amount of nonMatches: ${newNonMatches.length}`);\n }\n\n return {newMatches, newNonMatches};\n }\n\n function addQuery(matches) {\n debugData(`Adding query ${state.query} to matches`);\n return matches.map((match) => ({...match, matchQuery: state.query}));\n }\n\n function maxCandidatesRetrieved(candidateCount, maxCandidates) {\n debugData(`Total amount of candidate records retrieved: ${newCandidateCount} (max: ${maxCandidates})`);\n if (maxCandidates && candidateCount >= maxCandidates) {\n debug(`Stopped matching because maximum number of candidate records ${candidateCount} / ${maxCandidates} have been retrieved`);\n return true;\n }\n }\n }\n\n // matches : array of matching candidate records\n // nonMatches : array of nonMatching candidate records (if returnNonMatches option is true, otherwise empty array)\n // - candidate.id\n // - candidate.record\n // - probability\n // - strategy (if returnStrategy option is true)\n // - treshold (if returnStrategy option is true)\n // - matchQuery (if returnQuery option is true)\n\n // we could have here also returnRecords/returnMatchRecords/returnNonMatchRecord options that could be turned false for not to return actual record data\n\n // matchStatus.status: boolean, true if matcher retrieved and handled all found candidate records, false if it did not\n // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries',empty string/undefined), reason for stopping retrieving or handling the candidate records\n // - only one stopReason is returned (if there would be several possible stopReasons, stopReason is picked in the above order)\n // - 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\n\n function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount, nonMatchCount}) {\n checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount});\n const matchStatus = getMatchState(state, stopReason);\n // add nonMatches to result only if returnNonMatches is 'true', otherwise nonMatches have not been gathered\n const result = returnNonMatches ? {matches, matchStatus, nonMatches} : {matches, matchStatus};\n debugData(`${JSON.stringify(result)}`);\n return result;\n\n // note that in cases where the matching has been stopped because of maxMatches checkCounts won't (in most cases) match\n\n function checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount}) {\n const matchCount = matches.length;\n debugData(`Return nonMatches: ${returnNonMatches}`);\n const chosenNonMatchCount = returnNonMatches ? nonMatches.length : nonMatchCount;\n const totalHandled = matchCount + nonMatchCount + duplicateCount;\n debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}`);\n debug(`We got result for ${totalHandled} / ${candidateCount} retrieved candidates`);\n if (totalHandled !== candidateCount) {\n debug(`WARNING: Missing results for ${candidateCount - totalHandled} candidates`);\n return;\n }\n return;\n }\n\n function getMatchState(state, stopReason) {\n debugData(`${JSON.stringify(state)}`);\n 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}`);\n\n debugData(`StopReason: <${stopReason}>`);\n\n const searchesLeft = state.resultSetOffset && state.resultSetOffset <= state.totalRecords;\n const nonRetrieved = searchesLeft ? state.totalRecords - state.queryCandidateCounter : 0;\n debugData(`nonRetrieved: ${nonRetrieved}`);\n\n if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0) {\n const maxedQueriesStopReason = state.maxedQueries.length > 0 ? 'maxedQueries' : '';\n const newStopReason = stopReason === '' ? maxedQueriesStopReason : stopReason;\n debugData(`MaxedQueriesStopReason: <${maxedQueriesStopReason}>`);\n debugData(`NewStopReason: <${newStopReason}>`);\n debug(`Match status: false`);\n return {status: false, stopReason: newStopReason};\n }\n\n debug(`Match status: true`);\n return {status: true, stopReason};\n }\n }\n\n function iterateRecords({records, recordSetSize, maxMatches, matches = [], nonMatches = [], recordMatches = [], recordNonMatches = [], recordCount = 0, recordDuplicateCount = 0, recordNonMatchCount = 0}) {\n\n // recordSetSize : total amount of records in the current record set\n // recordCount : amount of records from the current record set that have been handled\n // maxMatches : setting for maximum amount found by current matcher job before the matcher job is stopped\n // recordDuplicateCount : amount of records from the current record set that are already included in matches/nonMatches results\n // recordNonMatchCount: amount of records from the current record set that are nonMatches (only is returnNonMatches setting is false)\n\n // records : non-handled records in the current record set\n // matches : found matches in the current matcher job\n // recordMatches : found matches in the current record set\n // recordNonMatches : found nonMatches in the current record set (only if returnNonMatches setting is true)\n\n const [candidate] = records;\n const newRecordCount = candidate ? recordCount + 1 : recordCount;\n\n // The matcher uses same matchDetection strategy for candidates from all candidate-searches -> matchDetection result for the same candidate is always same\n // Exceptions would happen if the candidate would have been updated in the database between candidate searches\n // 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\n // different candidate search queries. Same candidate search query won't have duplicate records.\n\n if (candidate) {\n\n if (candidateNotInMatches(matches.concat(nonMatches), candidate)) {\n const {record: candidateRecord, id: candidateId} = candidate;\n debug(`Running matchDetection for record ${candidateId} (${newRecordCount}/${recordSetSize})`);\n const detectionResult = detect(record, candidateRecord);\n return handleDetectionResult(detectionResult, candidateId, candidateRecord);\n }\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount: recordDuplicateCount + 1, recordNonMatchCount});\n }\n\n debug(`No more candidates, record set (${recordCount}/${recordSetSize}) done, ${recordMatches.length} matches found, ${recordDuplicateCount} candidates already handled, ${returnNonMatches ? `${recordNonMatches.length}` : `${recordNonMatchCount}`} nonMatches found.`);\n return {matches: recordMatches, nonMatches: returnNonMatches ? recordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: recordNonMatchCount};\n\n function handleDetectionResult(detectionResult, candidateId, candidateRecord) {\n debugData(`MatchDetection results for ${candidateId} (${newRecordCount}/${recordSetSize}): ${JSON.stringify(detectionResult)}`);\n\n if (detectionResult.match || returnNonMatches) {\n debug(`${detectionResult.match ? `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is a match!` : `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is NOT a match!`}`);\n debugData(`Strategy: ${JSON.stringify(detectionResult.strategy)}, Treshold: ${JSON.stringify(detectionResult.treshold)}`);\n\n const matchResult = {\n probability: detectionResult.probability,\n candidate: {\n id: candidateId,\n record: candidateRecord\n }\n };\n const strategyResult = {\n strategy: detectionResult.strategy,\n treshold: detectionResult.treshold\n };\n const newMatch = returnStrategy ? {...matchResult, ...strategyResult} : {...matchResult};\n\n debugData(`${JSON.stringify(newMatch)}`);\n\n return handleRecordMatch(detectionResult.match, newMatch);\n }\n\n const newRecordNonMatchCount = recordNonMatchCount + 1;\n debugData(`- Total nonMatches after this detection: ${newRecordNonMatchCount}`);\n\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount});\n }\n\n function handleRecordMatch(isMatch, newMatch) {\n const newRecordMatches = isMatch ? recordMatches.concat(newMatch) : recordMatches;\n const newRecordNonMatches = isMatch ? recordNonMatches : recordNonMatches.concat(newMatch);\n\n debugData(`- Total matches after this detection: ${matches.concat(newRecordMatches).length} (max: ${maxMatches})`);\n\n // eslint-disable-next-line functional/no-conditional-statement\n if (returnNonMatches) {\n debugData(`- Total nonMatches after this detection: ${nonMatches.concat(newRecordNonMatches).length}`);\n }\n\n if (maxMatchesFound({matches: matches.concat(newRecordMatches), maxMatches})) {\n debug(`MaxMatches (${maxMatches}) reached, handled candidates in record set: ${newRecordCount} non-handled candidates in record set ${recordSetSize - newRecordCount}`);\n return {matches: newRecordMatches, nonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: recordNonMatchCount};\n }\n\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, recordNonMatchCount});\n }\n\n function candidateNotInMatches(matches, candidate) {\n debug(`Checking that record ${candidate.id} is not already included in ${matches.length} matches/nonMatches`);\n const newCandidateId = candidate.id;\n debugData(`newCandidateId: ${newCandidateId}`);\n const result = matches.find(({candidate}) => candidate.id === newCandidateId);\n debugData(`Result: ${result}`);\n if (result) {\n debug(`${candidate.id} was already handled.`);\n return false;\n }\n debug(`${candidate.id} not found in matches/nonMatches`);\n return true;\n }\n }\n\n function maxMatchesFound({matches, maxMatches}) {\n if (maxMatches && matches.length >= maxMatches) {\n debug(`Stopping recordSet iteration: maxMatches (${maxMatches}) for matcher job found.`);\n return true;\n }\n }\n };\n};\n"],"file":"index.js"}
1
+ {"version":3,"sources":["../src/index.js"],"names":["detection","detectionOptions","search","searchOptions","maxMatches","maxCandidates","returnStrategy","returnQuery","returnNonMatches","returnFailures","debug","debugData","extend","JSON","stringify","detect","record","iterate","initialState","matches","candidateCount","nonMatches","duplicateCount","nonMatchCount","conversionFailures","records","failures","state","length","recordSetSize","failureSetSize","newCandidateCount","newConversionFailures","concat","handleRecordSet","queriesLeft","searchCounter","query","returnResult","stopReason","matchResult","iterateRecords","newDuplicateCount","newNonMatchCount","newMatches","newNonMatches","handleMatchResult","maxMatchesFound","maxCandidatesRetrieved","addQuery","map","match","matchQuery","conversionFailureCount","checkCounts","matchStatus","getMatchState","matchesResult","result","matchCount","chosenNonMatchCount","totalHandled","conversionFailuresCount","resultSetOffset","totalRecords","queryCandidateCounter","maxedQueries","searchesLeft","nonRetrieved","maxedQueriesStopReason","undefined","conversionFailuresStopReason","newStopReason","status","recordMatches","recordNonMatches","recordCount","recordDuplicateCount","recordNonMatchCount","candidate","newRecordCount","candidateNotInMatches","candidateRecord","id","candidateId","detectionResult","handleDetectionResult","slice","strategy","treshold","probability","strategyResult","newMatch","handleRecordMatch","newRecordNonMatchCount","isMatch","newRecordMatches","newRecordNonMatches","newCandidateId","find"],"mappings":"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","sourcesContent":["/**\n*\n* @licstart The following is the entire license notice for the JavaScript code in this file.\n*\n* Melinda record matching modules for Javascript\n*\n* Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)\n*\n* This file is part of melinda-record-matching-js\n*\n* melinda-record-matching-js program is free software: you can redistribute it and/or modify\n* it under the terms of the GNU Lesser General Public License as\n* published by the Free Software Foundation, either version 3 of the\n* License, or (at your option) any later version.\n*\n* melinda-record-matching-js is distributed in the hope that it will be useful,\n* but WITHOUT ANY WARRANTY; without even the implied warranty of\n* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n* GNU Lesser General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License\n* along with this program. If not, see <http://www.gnu.org/licenses/>.\n*\n* @licend The above is the entire license notice\n* for the JavaScript code in this file.\n*\n*/\n\nimport createDebugLogger from 'debug';\nimport createSearchInterface, * as candidateSearch from './candidate-search';\nimport createDetectionInterface, * as matchDetection from './match-detection';\n\nexport {candidateSearch, matchDetection};\n\nexport default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false, returnFailures}) => {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:index');\n const debugData = debug.extend('data');\n\n debugData(`DetectionOptions: ${JSON.stringify(detectionOptions)}`);\n debugData(`SearchOptions: ${JSON.stringify(searchOptions)}`);\n debugData(`MaxMatches: ${JSON.stringify(maxMatches)}`);\n debugData(`MaxCandidates: ${JSON.stringify(maxCandidates)}`);\n debugData(`ReturnStrategy: ${JSON.stringify(returnStrategy)}`);\n debugData(`ReturnQuery: ${JSON.stringify(returnQuery)}`);\n debugData(`ReturnNonMatches: ${JSON.stringify(returnNonMatches)}`);\n debugData(`ReturnFailures: ${JSON.stringify(returnFailures)}`);\n\n\n const detect = createDetectionInterface(detectionOptions, returnStrategy);\n\n return record => {\n const search = createSearchInterface({...searchOptions, record, maxCandidates});\n return iterate({});\n\n // candidateCount : amount of candidate records retrived from SRU for matching, NOT including current record set\n // matches : candidates that have been detected as matches by current matcher job\n // nonMatches : candidates that have been detected as non-matches by current matcher job (only if returnNonMatches is 'true')\n // duplicateCount : amount of candidate records that were retrieved from the SRU but not handled further because they were already found in the matches/nonMatches\n\n // state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)\n // state.query : current query (undefined if there was no queries left)\n // state.searchCounter : sequence for current search for current query (undefined, if there we no queries left)\n // 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)\n // state.queriesLeft : amount of queries left\n // state.queryCounter : sequence for current query\n // state.maxedQueries : queries that resulted in more than serverMaxResults hits\n\n async function iterate({initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0, nonMatchCount = 0, conversionFailures = []}) {\n debugData(`Starting next matcher iteration.`);\n const {records, failures, ...state} = await search(initialState);\n\n debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, nonMatchCount: ${nonMatchCount}, conversionFailures: ${conversionFailures}`);\n const recordSetSize = records.length;\n const failureSetSize = failures.length;\n const newCandidateCount = candidateCount + recordSetSize + failureSetSize;\n\n const newConversionFailures = conversionFailures.concat(failures);\n debugData(`Failures: ${failures.length}, ConversionFailures: ${conversionFailures.length}, NewConversionFailures: ${newConversionFailures.length}`);\n\n if (recordSetSize > 0) {\n return handleRecordSet();\n }\n\n if (state.queriesLeft > 0) {\n debug(`Empty record set ${state.searchCounter} for ${state.query}, but there are ${state.queriesLeft} queries left`);\n return iterate({initialState: state, matches, candidateCount: newCandidateCount, nonMatches, nonMatchCount, duplicateCount, conversionFailures: newConversionFailures});\n }\n\n debug(`No (more) candidate records to check, no more queries left, matches: ${matches.length}`);\n return returnResult({matches, state, stopReason: '', nonMatches, nonMatchCount, candidateCount: newCandidateCount, duplicateCount, conversionFailures: newConversionFailures});\n\n function handleRecordSet() {\n debug(`Checking record set of ${recordSetSize} candidate records for possible matches, found by ${state.searchCounter} search for ${state.query}`);\n\n const matchResult = iterateRecords({records, recordSetSize, maxMatches, matches, nonMatches, nonMatchCount});\n\n const newDuplicateCount = duplicateCount + matchResult.duplicateCount;\n const newNonMatchCount = nonMatchCount + matchResult.nonMatchCount;\n const {newMatches, newNonMatches} = handleMatchResult(matchResult, matches, nonMatches);\n\n if (maxMatchesFound({matches: newMatches, maxMatches})) {\n return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures});\n }\n\n if (maxCandidatesRetrieved(newCandidateCount, maxCandidates)) {\n return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures});\n }\n\n return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures});\n }\n\n function handleMatchResult(matchResult, matches, nonMatches) {\n debugData(`- Amount of new matches from record set: ${matchResult.matches.length}`);\n // eslint-disable-next-line functional/no-conditional-statement\n if (returnNonMatches) {\n debugData(`- Amount of new nonMatches from record set: ${matchResult.nonMatches.length}`);\n }\n\n const newMatches = matches.concat(returnQuery ? addQuery(matchResult.matches) : matchResult.matches);\n const newNonMatches = returnNonMatches ? nonMatches.concat(returnQuery ? addQuery(matchResult.nonMatches) : matchResult.nonMatches) : [];\n\n debugData(`- Total amount of matches: ${newMatches.length}`);\n // eslint-disable-next-line functional/no-conditional-statement\n if (returnNonMatches) {\n debugData(`- Total amount of nonMatches: ${newNonMatches.length}`);\n }\n\n return {newMatches, newNonMatches};\n }\n\n function addQuery(matches) {\n debugData(`Adding query ${state.query} to matches`);\n return matches.map((match) => ({...match, matchQuery: state.query}));\n }\n\n function maxCandidatesRetrieved(candidateCount, maxCandidates) {\n debugData(`Total amount of candidate records retrieved: ${newCandidateCount} (max: ${maxCandidates})`);\n if (maxCandidates && candidateCount >= maxCandidates) {\n debug(`Stopped matching because maximum number of candidate records ${candidateCount} / ${maxCandidates} have been retrieved`);\n return true;\n }\n }\n }\n\n // matches : array of matching candidate records\n // nonMatches : array of nonMatching candidate records (if returnNonMatches option is true, otherwise empty array)\n // - candidate.id\n // - candidate.record\n // - probability\n // - strategy (if returnStrategy option is true)\n // - treshold (if returnStrategy option is true)\n // - matchQuery (if returnQuery option is true)\n\n // we could have here also returnRecords/returnMatchRecords/returnNonMatchRecord options that could be turned false for not to return actual record data\n\n // matchStatus.status: boolean, true if matcher retrieved and handled all found candidate records, false if it did not\n // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries','conversionFailures', empty string/undefined), reason for stopping retrieving or handling the candidate records\n // - only one stopReason is returned (if there would be several possible stopReasons, stopReason is picked in the above order)\n // - 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\n\n function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount, nonMatchCount, conversionFailures}) {\n const conversionFailureCount = conversionFailures.length;\n checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, conversionFailureCount});\n const matchStatus = getMatchState(state, stopReason, conversionFailureCount);\n // add nonMatches to result only if returnNonMatches is 'true', otherwise nonMatches have not been gathered\n const matchesResult = returnNonMatches ? {matches, matchStatus, nonMatches} : {matches, matchStatus};\n const result = returnFailures ? {...matchesResult, conversionFailures} : matchesResult;\n debugData(`ReturnFailures ${returnFailures}`);\n debugData(`${JSON.stringify(result)}`);\n return result;\n\n // note that in cases where the matching has been stopped because of maxMatches checkCounts won't (in most cases) match\n\n function checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, conversionFailureCount}) {\n const matchCount = matches.length;\n debugData(`Return nonMatches: ${returnNonMatches}`);\n const chosenNonMatchCount = returnNonMatches ? nonMatches.length : nonMatchCount;\n const totalHandled = matchCount + chosenNonMatchCount + duplicateCount;\n debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}, conversionFailureCount: ${conversionFailureCount}`);\n debug(`We got result for ${totalHandled} / ${candidateCount} retrieved candidates`);\n if (totalHandled !== candidateCount) {\n debug(`WARNING: Missing results for ${candidateCount - totalHandled} candidates`);\n return;\n }\n return;\n }\n\n function getMatchState(state, stopReason, conversionFailuresCount) {\n debugData(`${JSON.stringify(state)}`);\n debug(`We had ${conversionFailuresCount} retrieved candidates that could not be converted.`);\n 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}`);\n\n debugData(`StopReason: <${stopReason}>`);\n\n const searchesLeft = state.resultSetOffset && state.resultSetOffset <= state.totalRecords;\n const nonRetrieved = searchesLeft ? state.totalRecords - state.queryCandidateCounter : 0;\n debugData(`nonRetrieved: ${nonRetrieved}`);\n\n // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries','conversionFailures', empty string/undefined), reason for stopping retrieving or handling the candidate records\n // 'maxMatches' and 'maxCandidates' are in stopReason, 'maxedQueries' and 'conversionFailures' are created here\n\n if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0 || conversionFailureCount > 0) {\n const maxedQueriesStopReason = state.maxedQueries.length > 0 ? 'maxedQueries' : undefined;\n const conversionFailuresStopReason = conversionFailureCount > 0 ? 'conversionFailures' : undefined;\n const newStopReason = stopReason === '' || stopReason === undefined ? maxedQueriesStopReason || conversionFailuresStopReason : stopReason;\n debugData(`MaxedQueriesStopReason: <${maxedQueriesStopReason}>`);\n debugData(`ConversionFailureStopReason <${conversionFailuresStopReason}>`);\n debugData(`NewStopReason: <${newStopReason}>`);\n debug(`Match status: false`);\n return {status: false, stopReason: newStopReason};\n }\n\n debug(`Match status: true`);\n return {status: true, stopReason};\n }\n }\n\n function iterateRecords({records, recordSetSize, maxMatches, matches = [], nonMatches = [], recordMatches = [], recordNonMatches = [], recordCount = 0, recordDuplicateCount = 0, recordNonMatchCount = 0}) {\n\n // recordSetSize : total amount of records in the current record set\n // recordCount : amount of records from the current record set that have been handled\n // maxMatches : setting for maximum amount found by current matcher job before the matcher job is stopped\n // recordDuplicateCount : amount of records from the current record set that are already included in matches/nonMatches results\n // recordNonMatchCount: amount of records from the current record set that are nonMatches (only is returnNonMatches setting is false)\n\n // records : non-handled records in the current record set\n // matches : found matches in the current matcher job\n // recordMatches : found matches in the current record set\n // recordNonMatches : found nonMatches in the current record set (only if returnNonMatches setting is true)\n\n const [candidate] = records;\n const newRecordCount = candidate ? recordCount + 1 : recordCount;\n\n // The matcher uses same matchDetection strategy for candidates from all candidate-searches -> matchDetection result for the same candidate is always same\n // Exceptions would happen if the candidate would have been updated in the database between candidate searches\n // 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\n // different candidate search queries. Same candidate search query won't have duplicate records.\n\n if (candidate) {\n\n if (candidateNotInMatches(matches.concat(nonMatches), candidate)) {\n const {record: candidateRecord, id: candidateId} = candidate;\n debug(`Running matchDetection for record ${candidateId} (${newRecordCount}/${recordSetSize})`);\n const detectionResult = detect(record, candidateRecord);\n return handleDetectionResult(detectionResult, candidateId, candidateRecord);\n }\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount: recordDuplicateCount + 1, recordNonMatchCount});\n }\n\n debug(`No more candidates, record set (${recordCount}/${recordSetSize}) done, ${recordMatches.length} matches found, ${recordDuplicateCount} candidates already handled, ${returnNonMatches ? `${recordNonMatches.length}` : `${recordNonMatchCount}`} nonMatches found.`);\n return {matches: recordMatches, nonMatches: returnNonMatches ? recordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: recordNonMatchCount};\n\n function handleDetectionResult(detectionResult, candidateId, candidateRecord) {\n debugData(`MatchDetection results for ${candidateId} (${newRecordCount}/${recordSetSize}): ${JSON.stringify(detectionResult)}`);\n\n if (detectionResult.match || returnNonMatches) {\n debug(`${detectionResult.match ? `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is a match!` : `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is NOT a match!`}`);\n debugData(`Strategy: ${JSON.stringify(detectionResult.strategy)}, Treshold: ${JSON.stringify(detectionResult.treshold)}`);\n\n const matchResult = {\n probability: detectionResult.probability,\n candidate: {\n id: candidateId,\n record: candidateRecord\n }\n };\n const strategyResult = {\n strategy: detectionResult.strategy,\n treshold: detectionResult.treshold\n };\n const newMatch = returnStrategy ? {...matchResult, ...strategyResult} : {...matchResult};\n\n debugData(`${JSON.stringify(newMatch)}`);\n\n return handleRecordMatch(detectionResult.match, newMatch);\n }\n\n const newRecordNonMatchCount = recordNonMatchCount + 1;\n debugData(`- Total nonMatches after this detection: ${newRecordNonMatchCount}`);\n\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount});\n }\n\n function handleRecordMatch(isMatch, newMatch) {\n const newRecordMatches = isMatch ? recordMatches.concat(newMatch) : recordMatches;\n const newRecordNonMatches = isMatch ? recordNonMatches : recordNonMatches.concat(newMatch);\n const newRecordNonMatchCount = isMatch ? recordNonMatchCount : recordNonMatchCount + 1;\n\n debugData(`- Total matches after this detection: ${matches.concat(newRecordMatches).length} (max: ${maxMatches})`);\n\n // eslint-disable-next-line functional/no-conditional-statement\n if (returnNonMatches) {\n debugData(`- Total nonMatches after this detection: ${nonMatches.concat(newRecordNonMatches).length}`);\n }\n debugData(`- Total nonMatchCount after this detection: ${recordNonMatchCount}`);\n\n if (maxMatchesFound({matches: matches.concat(newRecordMatches), maxMatches})) {\n debug(`MaxMatches (${maxMatches}) reached, handled candidates in record set: ${newRecordCount} non-handled candidates in record set ${recordSetSize - newRecordCount}`);\n return {matches: newRecordMatches, nonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: newRecordNonMatchCount};\n }\n\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount});\n }\n\n function candidateNotInMatches(matches, candidate) {\n debug(`Checking that record ${candidate.id} is not already included in ${matches.length} matches/nonMatches`);\n const newCandidateId = candidate.id;\n debugData(`newCandidateId: ${newCandidateId}`);\n const result = matches.find(({candidate}) => candidate.id === newCandidateId);\n debugData(`Result: ${result}`);\n if (result) {\n debug(`${candidate.id} was already handled.`);\n return false;\n }\n debug(`${candidate.id} not found in matches/nonMatches`);\n return true;\n }\n }\n\n function maxMatchesFound({matches, maxMatches}) {\n if (maxMatches && matches.length >= maxMatches) {\n debug(`Stopping recordSet iteration: maxMatches (${maxMatches}) for matcher job found.`);\n return true;\n }\n }\n };\n};\n"],"file":"index.js"}
@@ -62,7 +62,8 @@ describe('INDEX', () => {
62
62
  options,
63
63
  enabled = true,
64
64
  expectedMatchStatus,
65
- expectedStopReason
65
+ expectedStopReason,
66
+ expectedFailures
66
67
  }) {
67
68
  if (!enabled) {
68
69
  debug(`Disabled test!`);
@@ -78,15 +79,20 @@ describe('INDEX', () => {
78
79
  const {
79
80
  matches,
80
81
  matchStatus,
81
- nonMatches
82
+ nonMatches,
83
+ conversionFailures
82
84
  } = await match(record);
83
- debugData(`${matches.length}, ${matchStatus.status}/${matchStatus.stopReason}, ${nonMatches ? nonMatches.length : nonMatches}`);
85
+ debugData(`Matches: ${matches.length}, Status: ${matchStatus.status}/${matchStatus.stopReason}, NonMatches: ${nonMatches ? nonMatches.length : 'not returned'}, ConversionFailures: ${conversionFailures ? conversionFailures.length : 'not returned'}`);
86
+ (0, _chai.expect)(matchStatus.status).to.eql(expectedMatchStatus);
87
+ (0, _chai.expect)(matchStatus.stopReason).to.eql(expectedStopReason);
84
88
  const formattedMatchResult = formatRecordResults(matches);
85
89
  (0, _chai.expect)(formattedMatchResult).to.eql(expectedMatches);
86
90
  const formattedNonMatchResult = formatRecordResults(nonMatches);
87
- (0, _chai.expect)(formattedNonMatchResult).to.eql(expectedNonMatches);
88
- (0, _chai.expect)(matchStatus.status).to.eql(expectedMatchStatus);
89
- (0, _chai.expect)(matchStatus.stopReason).to.eql(expectedStopReason);
91
+ (0, _chai.expect)(formattedNonMatchResult).to.eql(expectedNonMatches); // eslint-disable-next-line functional/no-conditional-statement
92
+
93
+ if (expectedFailures) {
94
+ (0, _chai.expect)(conversionFailures).to.eql(expectedFailures);
95
+ }
90
96
 
91
97
  function formatOptions() {
92
98
  const contextFeatures = _.matchDetection.features[options.detection.strategy.type];
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.spec.js"],"names":["debug","debugData","extend","describe","callback","path","__dirname","recurse","fixura","reader","READERS","JSON","getFixture","options","enabled","expectedMatchStatus","expectedStopReason","record","MarcRecord","subfieldValues","expectedMatches","expectedNonMatches","match","formatOptions","matches","matchStatus","nonMatches","length","status","stopReason","formattedMatchResult","formatRecordResults","to","eql","formattedNonMatchResult","contextFeatures","matchDetection","features","detection","strategy","type","search","detect","map","v","stringify","candidate","formatCandidate","id","newId","newRecord","toObject"],"mappings":"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","sourcesContent":["/**\n*\n* @licstart The following is the entire license notice for the JavaScript code in this file.\n*\n* Melinda record matching modules for Javascript\n*\n* Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)\n*\n* This file is part of melinda-record-matching-js\n*\n* melinda-record-matching-js program is free software: you can redistribute it and/or modify\n* it under the terms of the GNU Lesser General Public License as\n* published by the Free Software Foundation, either version 3 of the\n* License, or (at your option) any later version.\n*\n* melinda-record-matching-js is distributed in the hope that it will be useful,\n* but WITHOUT ANY WARRANTY; without even the implied warranty of\n* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n* GNU Lesser General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License\n* along with this program. If not, see <http://www.gnu.org/licenses/>.\n*\n* @licend The above is the entire license notice\n* for the JavaScript code in this file.\n*\n*/\n\nimport {expect} from 'chai';\nimport {READERS} from '@natlibfi/fixura';\nimport generateTests from '@natlibfi/fixugen-http-client';\nimport {MarcRecord} from '@natlibfi/marc-record';\nimport createMatchInterface, {matchDetection} from '.';\nimport createDebugLogger from 'debug';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:index:test');\nconst debugData = debug.extend('data');\n\ndescribe('INDEX', () => {\n generateTests({\n callback,\n path: [__dirname, '..', 'test-fixtures', 'index'],\n recurse: false,\n fixura: {\n reader: READERS.JSON\n }\n });\n\n async function callback({getFixture, options, enabled = true, expectedMatchStatus, expectedStopReason}) {\n\n if (!enabled) {\n debug(`Disabled test!`);\n return;\n }\n\n const record = new MarcRecord(getFixture('inputRecord.json'), {subfieldValues: false});\n const expectedMatches = getFixture('expectedMatches.json');\n const expectedNonMatches = getFixture('expectedNonMatches.json') || [];\n\n\n const match = createMatchInterface(formatOptions());\n const {matches, matchStatus, nonMatches} = await match(record);\n debugData(`${matches.length}, ${matchStatus.status}/${matchStatus.stopReason}, ${nonMatches ? nonMatches.length : nonMatches}`);\n\n const formattedMatchResult = formatRecordResults(matches);\n expect(formattedMatchResult).to.eql(expectedMatches);\n\n const formattedNonMatchResult = formatRecordResults(nonMatches);\n expect(formattedNonMatchResult).to.eql(expectedNonMatches);\n\n expect(matchStatus.status).to.eql(expectedMatchStatus);\n expect(matchStatus.stopReason).to.eql(expectedStopReason);\n\n\n function formatOptions() {\n const contextFeatures = matchDetection.features[options.detection.strategy.type];\n\n return {\n ...options,\n search: {\n ...options.search\n },\n detection: {\n ...options.detect,\n strategy: options.detection.strategy.features.map(v => contextFeatures[v]())\n }\n };\n }\n\n function formatRecordResults(matches) {\n if (matches) {\n debugData(JSON.stringify(matches));\n return matches.map((match) => ({\n ...match,\n candidate: formatCandidate(match.candidate)\n }));\n }\n return [];\n }\n\n // Format candidate to remove validationOptions from record\n function formatCandidate({id, record}) {\n const newId = id;\n const newRecord = record;\n return {\n id: newId,\n record: newRecord.toObject()\n };\n }\n\n\n }\n\n});\n"],"file":"index.spec.js"}
1
+ {"version":3,"sources":["../src/index.spec.js"],"names":["debug","debugData","extend","describe","callback","path","__dirname","recurse","fixura","reader","READERS","JSON","getFixture","options","enabled","expectedMatchStatus","expectedStopReason","expectedFailures","record","MarcRecord","subfieldValues","expectedMatches","expectedNonMatches","match","formatOptions","matches","matchStatus","nonMatches","conversionFailures","length","status","stopReason","to","eql","formattedMatchResult","formatRecordResults","formattedNonMatchResult","contextFeatures","matchDetection","features","detection","strategy","type","search","detect","map","v","stringify","candidate","formatCandidate","id","newId","newRecord","toObject"],"mappings":";;AA4BA;;AACA;;AACA;;AACA;;AACA;;AACA;;;;;;;;AAjCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AASA,MAAMA,KAAK,GAAG,oBAAkB,8CAAlB,CAAd;AACA,MAAMC,SAAS,GAAGD,KAAK,CAACE,MAAN,CAAa,MAAb,CAAlB;AAEAC,QAAQ,CAAC,OAAD,EAAU,MAAM;AACtB,kCAAc;AACZC,IAAAA,QADY;AAEZC,IAAAA,IAAI,EAAE,CAACC,SAAD,EAAY,IAAZ,EAAkB,eAAlB,EAAmC,OAAnC,CAFM;AAGZC,IAAAA,OAAO,EAAE,KAHG;AAIZC,IAAAA,MAAM,EAAE;AACNC,MAAAA,MAAM,EAAEC,gBAAQC;AADV;AAJI,GAAd;;AASA,iBAAeP,QAAf,CAAwB;AAACQ,IAAAA,UAAD;AAAaC,IAAAA,OAAb;AAAsBC,IAAAA,OAAO,GAAG,IAAhC;AAAsCC,IAAAA,mBAAtC;AAA2DC,IAAAA,kBAA3D;AAA+EC,IAAAA;AAA/E,GAAxB,EAA0H;AAExH,QAAI,CAACH,OAAL,EAAc;AACZd,MAAAA,KAAK,CAAE,gBAAF,CAAL;AACA;AACD;;AAED,UAAMkB,MAAM,GAAG,IAAIC,sBAAJ,CAAeP,UAAU,CAAC,kBAAD,CAAzB,EAA+C;AAACQ,MAAAA,cAAc,EAAE;AAAjB,KAA/C,CAAf;AACA,UAAMC,eAAe,GAAGT,UAAU,CAAC,sBAAD,CAAlC;AACA,UAAMU,kBAAkB,GAAGV,UAAU,CAAC,yBAAD,CAAV,IAAyC,EAApE;AAGA,UAAMW,KAAK,GAAG,eAAqBC,aAAa,EAAlC,CAAd;AACA,UAAM;AAACC,MAAAA,OAAD;AAAUC,MAAAA,WAAV;AAAuBC,MAAAA,UAAvB;AAAmCC,MAAAA;AAAnC,QAAyD,MAAML,KAAK,CAACL,MAAD,CAA1E;AACAjB,IAAAA,SAAS,CAAE,YAAWwB,OAAO,CAACI,MAAO,aAAYH,WAAW,CAACI,MAAO,IAAGJ,WAAW,CAACK,UAAW,iBAAgBJ,UAAU,GAAGA,UAAU,CAACE,MAAd,GAAuB,cAAe,yBAAwBD,kBAAkB,GAAGA,kBAAkB,CAACC,MAAtB,GAA+B,cAAe,EAA7O,CAAT;AAEA,sBAAOH,WAAW,CAACI,MAAnB,EAA2BE,EAA3B,CAA8BC,GAA9B,CAAkClB,mBAAlC;AACA,sBAAOW,WAAW,CAACK,UAAnB,EAA+BC,EAA/B,CAAkCC,GAAlC,CAAsCjB,kBAAtC;AAEA,UAAMkB,oBAAoB,GAAGC,mBAAmB,CAACV,OAAD,CAAhD;AACA,sBAAOS,oBAAP,EAA6BF,EAA7B,CAAgCC,GAAhC,CAAoCZ,eAApC;AAEA,UAAMe,uBAAuB,GAAGD,mBAAmB,CAACR,UAAD,CAAnD;AACA,sBAAOS,uBAAP,EAAgCJ,EAAhC,CAAmCC,GAAnC,CAAuCX,kBAAvC,EAvBwH,CAyBxH;;AACA,QAAIL,gBAAJ,EAAsB;AACpB,wBAAOW,kBAAP,EAA2BI,EAA3B,CAA8BC,GAA9B,CAAkChB,gBAAlC;AACD;;AAED,aAASO,aAAT,GAAyB;AACvB,YAAMa,eAAe,GAAGC,iBAAeC,QAAf,CAAwB1B,OAAO,CAAC2B,SAAR,CAAkBC,QAAlB,CAA2BC,IAAnD,CAAxB;AAEA,aAAO,EACL,GAAG7B,OADE;AAEL8B,QAAAA,MAAM,EAAE,EACN,GAAG9B,OAAO,CAAC8B;AADL,SAFH;AAKLH,QAAAA,SAAS,EAAE,EACT,GAAG3B,OAAO,CAAC+B,MADF;AAETH,UAAAA,QAAQ,EAAE5B,OAAO,CAAC2B,SAAR,CAAkBC,QAAlB,CAA2BF,QAA3B,CAAoCM,GAApC,CAAwCC,CAAC,IAAIT,eAAe,CAACS,CAAD,CAAf,EAA7C;AAFD;AALN,OAAP;AAUD;;AAED,aAASX,mBAAT,CAA6BV,OAA7B,EAAsC;AACpC,UAAIA,OAAJ,EAAa;AACXxB,QAAAA,SAAS,CAACU,IAAI,CAACoC,SAAL,CAAetB,OAAf,CAAD,CAAT;AACA,eAAOA,OAAO,CAACoB,GAAR,CAAatB,KAAD,KAAY,EAC7B,GAAGA,KAD0B;AAE7ByB,UAAAA,SAAS,EAAEC,eAAe,CAAC1B,KAAK,CAACyB,SAAP;AAFG,SAAZ,CAAZ,CAAP;AAID;;AACD,aAAO,EAAP;AACD,KAtDuH,CAwDxH;;;AACA,aAASC,eAAT,CAAyB;AAACC,MAAAA,EAAD;AAAKhC,MAAAA;AAAL,KAAzB,EAAuC;AACrC,YAAMiC,KAAK,GAAGD,EAAd;AACA,YAAME,SAAS,GAAGlC,MAAlB;AACA,aAAO;AACLgC,QAAAA,EAAE,EAAEC,KADC;AAELjC,QAAAA,MAAM,EAAEkC,SAAS,CAACC,QAAV;AAFH,OAAP;AAID;AAGF;AAEF,CA/EO,CAAR","sourcesContent":["/**\n*\n* @licstart The following is the entire license notice for the JavaScript code in this file.\n*\n* Melinda record matching modules for Javascript\n*\n* Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)\n*\n* This file is part of melinda-record-matching-js\n*\n* melinda-record-matching-js program is free software: you can redistribute it and/or modify\n* it under the terms of the GNU Lesser General Public License as\n* published by the Free Software Foundation, either version 3 of the\n* License, or (at your option) any later version.\n*\n* melinda-record-matching-js is distributed in the hope that it will be useful,\n* but WITHOUT ANY WARRANTY; without even the implied warranty of\n* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n* GNU Lesser General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License\n* along with this program. If not, see <http://www.gnu.org/licenses/>.\n*\n* @licend The above is the entire license notice\n* for the JavaScript code in this file.\n*\n*/\n\nimport {expect} from 'chai';\nimport {READERS} from '@natlibfi/fixura';\nimport generateTests from '@natlibfi/fixugen-http-client';\nimport {MarcRecord} from '@natlibfi/marc-record';\nimport createMatchInterface, {matchDetection} from '.';\nimport createDebugLogger from 'debug';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:index:test');\nconst debugData = debug.extend('data');\n\ndescribe('INDEX', () => {\n generateTests({\n callback,\n path: [__dirname, '..', 'test-fixtures', 'index'],\n recurse: false,\n fixura: {\n reader: READERS.JSON\n }\n });\n\n async function callback({getFixture, options, enabled = true, expectedMatchStatus, expectedStopReason, expectedFailures}) {\n\n if (!enabled) {\n debug(`Disabled test!`);\n return;\n }\n\n const record = new MarcRecord(getFixture('inputRecord.json'), {subfieldValues: false});\n const expectedMatches = getFixture('expectedMatches.json');\n const expectedNonMatches = getFixture('expectedNonMatches.json') || [];\n\n\n const match = createMatchInterface(formatOptions());\n const {matches, matchStatus, nonMatches, conversionFailures} = await match(record);\n debugData(`Matches: ${matches.length}, Status: ${matchStatus.status}/${matchStatus.stopReason}, NonMatches: ${nonMatches ? nonMatches.length : 'not returned'}, ConversionFailures: ${conversionFailures ? conversionFailures.length : 'not returned'}`);\n\n expect(matchStatus.status).to.eql(expectedMatchStatus);\n expect(matchStatus.stopReason).to.eql(expectedStopReason);\n\n const formattedMatchResult = formatRecordResults(matches);\n expect(formattedMatchResult).to.eql(expectedMatches);\n\n const formattedNonMatchResult = formatRecordResults(nonMatches);\n expect(formattedNonMatchResult).to.eql(expectedNonMatches);\n\n // eslint-disable-next-line functional/no-conditional-statement\n if (expectedFailures) {\n expect(conversionFailures).to.eql(expectedFailures);\n }\n\n function formatOptions() {\n const contextFeatures = matchDetection.features[options.detection.strategy.type];\n\n return {\n ...options,\n search: {\n ...options.search\n },\n detection: {\n ...options.detect,\n strategy: options.detection.strategy.features.map(v => contextFeatures[v]())\n }\n };\n }\n\n function formatRecordResults(matches) {\n if (matches) {\n debugData(JSON.stringify(matches));\n return matches.map((match) => ({\n ...match,\n candidate: formatCandidate(match.candidate)\n }));\n }\n return [];\n }\n\n // Format candidate to remove validationOptions from record\n function formatCandidate({id, record}) {\n const newId = id;\n const newRecord = record;\n return {\n id: newId,\n record: newRecord.toObject()\n };\n }\n\n\n }\n\n});\n"],"file":"index.spec.js"}
package/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "url": "git@github.com:natlibfi/melinda-record-matching-js.git"
15
15
  },
16
16
  "license": "LGPL-3.0+",
17
- "version": "2.1.0",
17
+ "version": "2.2.0-alpha.1",
18
18
  "main": "./dist/index.js",
19
19
  "engines": {
20
20
  "node": ">=14"
@@ -37,31 +37,32 @@
37
37
  "dependencies": {
38
38
  "@natlibfi/marc-record": "^7.0.0",
39
39
  "@natlibfi/marc-record-serializers": "^8.1.0",
40
- "@natlibfi/sru-client": "^5.0.0",
41
- "debug": "^4.3.3",
42
- "moment": "^2.29.1",
40
+ "@natlibfi/melinda-commons": "^12.0.3",
41
+ "@natlibfi/sru-client": "^5.0.1",
42
+ "debug": "^4.3.4",
43
+ "moment": "^2.29.2",
43
44
  "natural": "^5.1.13",
44
45
  "uuid": "^8.3.2",
45
- "winston": "^3.3.4"
46
+ "winston": "^3.7.2"
46
47
  },
47
48
  "devDependencies": {
48
- "@babel/cli": "^7.16.7",
49
- "@babel/core": "^7.16.7",
50
- "@babel/eslint-parser": "^7.16.5",
51
- "@babel/node": "^7.16.7",
52
- "@babel/preset-env": "^7.16.7",
53
- "@babel/register": "^7.16.7",
49
+ "@babel/cli": "^7.17.6",
50
+ "@babel/core": "^7.17.9",
51
+ "@babel/eslint-parser": "^7.17.0",
52
+ "@babel/node": "^7.16.8",
53
+ "@babel/preset-env": "^7.16.11",
54
+ "@babel/register": "^7.17.7",
54
55
  "@natlibfi/eslint-config-melinda-backend": "^2.0.0",
55
56
  "@natlibfi/fixugen": "^1.0.2",
56
- "@natlibfi/fixugen-http-client": "^1.1.3",
57
+ "@natlibfi/fixugen-http-client": "^2.0.1",
57
58
  "@natlibfi/fixura": "^2.2.1",
58
59
  "babel-plugin-istanbul": "^6.1.1",
59
60
  "babel-plugin-rewire": "^1.2.0",
60
- "chai": "^4.3.4",
61
+ "chai": "^4.3.6",
61
62
  "chai-as-promised": "^7.1.1",
62
63
  "cross-env": "^7.0.3",
63
- "eslint": "^8.6.0",
64
- "mocha": "^9.1.3",
64
+ "eslint": "^8.13.0",
65
+ "mocha": "^9.2.2",
65
66
  "nodemon": "^2.0.15",
66
67
  "nyc": "^15.1.0"
67
68
  },
@@ -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
 
@@ -72,40 +73,39 @@ export default ({record, searchSpec, url, maxCandidates, maxRecordsPerRequest =
72
73
  // state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)
73
74
  // state.query : current query (undefined if there was no queries left)
74
75
  // state.searchCounter : sequence for current search for current query (undefined, if there we no queries left)
75
- // 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)
76
77
  // state.queriesLeft : amount of queries left
77
78
  // state.queryCounter : sequence for current query
78
79
  // state.maxedQueries : queries that resulted in more than serverMaxResults hits
79
80
 
80
81
 
81
- // eslint-disable-next-line max-statements
82
82
  return async ({queryOffset = 0, resultSetOffset = 1, totalRecords = 0, searchCounter = 0, queryCandidateCounter = 0, queryCounter = 0, maxedQueries = []}) => {
83
83
  const query = queryList[queryOffset];
84
84
 
85
85
  if (query) {
86
- const {records, nextOffset, total} = await retrieveRecords();
86
+ const {records, failures, nextOffset, total} = await retrieveRecords();
87
87
 
88
88
  // If resultSetOffset === 1 this is the first search for the current query
89
89
  debugData(`ResultSetOffset: ${resultSetOffset}`);
90
90
  const newTotalRecords = resultSetOffset === 1 ? total : totalRecords;
91
91
  const newQueryCounter = resultSetOffset === 1 ? queryCounter + 1 : queryCounter;
92
92
  const newSearchCounter = resultSetOffset === 1 ? 1 : searchCounter + 1;
93
- const newQueryCandidateCounter = resultSetOffset === 1 ? records.length : queryCandidateCounter + records.length;
93
+ const newQueryCandidateCounter = resultSetOffset === 1 ? records.length + failures.length : queryCandidateCounter + records.length + failures.length;
94
94
 
95
95
  const maxedQuery = resultSetOffset === 1 ? checkMaxedQuery(query, total, serverMaxResult) : undefined;
96
96
  const newMaxedQueries = maxedQuery ? maxedQueries.concat(maxedQuery) : maxedQueries;
97
97
 
98
98
  if (typeof nextOffset === 'number') {
99
99
  debug(`Next search will be for query ${queryOffset} ${query}, starting from record ${nextOffset}`);
100
- return {records, queryOffset, resultSetOffset: nextOffset, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCounter, 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};
101
101
  }
102
102
  debug(`Query ${queryOffset} ${query} done.`);
103
103
  debug(`There are (${queryList.length - (queryOffset + 1)} queries left)`);
104
- 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};
105
105
  }
106
106
 
107
107
  debug(`All ${queryList.length} queries done, there's no query for ${queryOffset}`);
108
- return {records: [], queriesLeft: 0, queryCounter, maxedQueries};
108
+ return {records: [], failures: [], queriesLeft: 0, queryCounter, maxedQueries};
109
109
 
110
110
  function retrieveRecords() {
111
111
  return new Promise((resolve, reject) => {
@@ -131,27 +131,39 @@ export default ({record, searchSpec, url, maxCandidates, maxRecordsPerRequest =
131
131
  })
132
132
  .on('end', async nextOffset => {
133
133
  try {
134
- const records = await Promise.all(promises);
135
- 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}));
136
138
 
137
- debug(`Found ${filtered.length} candidates`);
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)}.`);
138
144
 
139
- resolve({nextOffset, records: filtered, total: totalRecords});
145
+
146
+ resolve({nextOffset, records: filtered, failures, total: totalRecords});
140
147
  } catch (err) {
148
+ debug(`Error caught on END`);
141
149
  reject(err);
142
150
  }
143
151
  })
144
- .on('record', foundRecord => {
152
+ .on('record', foundRecordXML => {
145
153
  promises.push(handleRecord()); // eslint-disable-line functional/immutable-data
146
154
 
147
155
  async function handleRecord() {
148
156
  try {
149
- const foundRecordMarc = await MARCXML.from(foundRecord, {subfieldValues: false});
157
+ const foundRecordMarc = await MARCXML.from(foundRecordXML, {subfieldValues: false});
150
158
  const foundRecordId = getRecordId(foundRecordMarc);
151
159
 
152
160
  return {record: foundRecordMarc, id: foundRecordId};
153
161
  } catch (err) {
154
- 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});
155
167
  }
156
168
  }
157
169
  });
@@ -172,4 +184,11 @@ export default ({record, searchSpec, url, maxCandidates, maxRecordsPerRequest =
172
184
  const [field] = record.get(/^001$/u);
173
185
  return field ? field.value : '';
174
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
+
175
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
 
@@ -77,7 +78,7 @@ describe('candidate-search', () => {
77
78
  }
78
79
 
79
80
  // eslint-disable-next-line max-statements
80
- async function iterate({searchOptions, expectedSearchError, 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,8 +86,17 @@ 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
  }
@@ -98,7 +108,7 @@ describe('candidate-search', () => {
98
108
  }
99
109
 
100
110
  function formatResults(results) {
101
- // console.log(results); //eslint-disable-line
111
+ debug(results); //eslint-disable-line
102
112
  return {
103
113
  ...results,
104
114
  records: results.records.map(({record, id}) => ({id, record: record.toObject()}))
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,6 +43,8 @@ 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
 
@@ -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, nonMatchCount = 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,29 +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;
91
98
  const newNonMatchCount = nonMatchCount + matchResult.nonMatchCount;
92
99
  const {newMatches, newNonMatches} = handleMatchResult(matchResult, matches, nonMatches);
93
100
 
94
101
  if (maxMatchesFound({matches: newMatches, maxMatches})) {
95
- return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount});
102
+ return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures});
96
103
  }
97
104
 
98
105
  if (maxCandidatesRetrieved(newCandidateCount, maxCandidates)) {
99
- return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount});
106
+ return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures});
100
107
  }
101
108
 
102
- return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount});
109
+ return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures});
103
110
  }
104
111
 
105
112
  function handleMatchResult(matchResult, matches, nonMatches) {
@@ -147,26 +154,29 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
147
154
  // we could have here also returnRecords/returnMatchRecords/returnNonMatchRecord options that could be turned false for not to return actual record data
148
155
 
149
156
  // 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
157
+ // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries','conversionFailures', empty string/undefined), reason for stopping retrieving or handling the candidate records
151
158
  // - only one stopReason is returned (if there would be several possible stopReasons, stopReason is picked in the above order)
152
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
153
160
 
154
- function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount, nonMatchCount}) {
155
- checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount});
156
- 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);
157
165
  // add nonMatches to result only if returnNonMatches is 'true', otherwise nonMatches have not been gathered
158
- 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}`);
159
169
  debugData(`${JSON.stringify(result)}`);
160
170
  return result;
161
171
 
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, nonMatchCount}) {
174
+ function checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, conversionFailureCount}) {
165
175
  const matchCount = matches.length;
166
176
  debugData(`Return nonMatches: ${returnNonMatches}`);
167
177
  const chosenNonMatchCount = returnNonMatches ? nonMatches.length : nonMatchCount;
168
- const totalHandled = matchCount + nonMatchCount + duplicateCount;
169
- debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}`);
178
+ const totalHandled = matchCount + chosenNonMatchCount + duplicateCount;
179
+ debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}, conversionFailureCount: ${conversionFailureCount}`);
170
180
  debug(`We got result for ${totalHandled} / ${candidateCount} retrieved candidates`);
171
181
  if (totalHandled !== candidateCount) {
172
182
  debug(`WARNING: Missing results for ${candidateCount - totalHandled} candidates`);
@@ -175,8 +185,9 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
175
185
  return;
176
186
  }
177
187
 
178
- function getMatchState(state, stopReason) {
188
+ function getMatchState(state, stopReason, conversionFailuresCount) {
179
189
  debugData(`${JSON.stringify(state)}`);
190
+ debug(`We had ${conversionFailuresCount} retrieved candidates that could not be converted.`);
180
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}`);
181
192
 
182
193
  debugData(`StopReason: <${stopReason}>`);
@@ -185,10 +196,15 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
185
196
  const nonRetrieved = searchesLeft ? state.totalRecords - state.queryCandidateCounter : 0;
186
197
  debugData(`nonRetrieved: ${nonRetrieved}`);
187
198
 
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;
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;
191
206
  debugData(`MaxedQueriesStopReason: <${maxedQueriesStopReason}>`);
207
+ debugData(`ConversionFailureStopReason <${conversionFailuresStopReason}>`);
192
208
  debugData(`NewStopReason: <${newStopReason}>`);
193
209
  debug(`Match status: false`);
194
210
  return {status: false, stopReason: newStopReason};
@@ -268,6 +284,7 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
268
284
  function handleRecordMatch(isMatch, newMatch) {
269
285
  const newRecordMatches = isMatch ? recordMatches.concat(newMatch) : recordMatches;
270
286
  const newRecordNonMatches = isMatch ? recordNonMatches : recordNonMatches.concat(newMatch);
287
+ const newRecordNonMatchCount = isMatch ? recordNonMatchCount : recordNonMatchCount + 1;
271
288
 
272
289
  debugData(`- Total matches after this detection: ${matches.concat(newRecordMatches).length} (max: ${maxMatches})`);
273
290
 
@@ -275,13 +292,14 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
275
292
  if (returnNonMatches) {
276
293
  debugData(`- Total nonMatches after this detection: ${nonMatches.concat(newRecordNonMatches).length}`);
277
294
  }
295
+ debugData(`- Total nonMatchCount after this detection: ${recordNonMatchCount}`);
278
296
 
279
297
  if (maxMatchesFound({matches: matches.concat(newRecordMatches), maxMatches})) {
280
298
  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};
299
+ return {matches: newRecordMatches, nonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: newRecordNonMatchCount};
282
300
  }
283
301
 
284
- return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, recordNonMatchCount});
302
+ return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount});
285
303
  }
286
304
 
287
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];