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