@natlibfi/melinda-record-matching 4.4.0-alpha.1 → 5.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.github/workflows/melinda-node-tests.yml +13 -9
  2. package/LICENSE.txt +21 -165
  3. package/README.md +2 -2
  4. package/dist/candidate-search/candidate-search-utils.js +6 -43
  5. package/dist/candidate-search/candidate-search-utils.js.map +7 -1
  6. package/dist/candidate-search/choose-queries.js +75 -0
  7. package/dist/candidate-search/choose-queries.js.map +7 -0
  8. package/dist/candidate-search/index.js +89 -225
  9. package/dist/candidate-search/index.js.map +7 -1
  10. package/dist/candidate-search/index.test.js +92 -0
  11. package/dist/candidate-search/index.test.js.map +7 -0
  12. package/dist/candidate-search/query-list/bib.js +141 -320
  13. package/dist/candidate-search/query-list/bib.js.map +7 -1
  14. package/dist/candidate-search/query-list/bib.test.js +34 -0
  15. package/dist/candidate-search/query-list/bib.test.js.map +7 -0
  16. package/dist/candidate-search/query-list/component.js +87 -0
  17. package/dist/candidate-search/query-list/component.js.map +7 -0
  18. package/dist/candidate-search/query-list/index.js +39 -52
  19. package/dist/candidate-search/query-list/index.js.map +7 -1
  20. package/dist/cli.js +138 -0
  21. package/dist/cli.js.map +7 -0
  22. package/dist/index.js +87 -462
  23. package/dist/index.js.map +7 -1
  24. package/dist/index.test.js +69 -0
  25. package/dist/index.test.js.map +7 -0
  26. package/dist/match-detection/features/bib/all-source-ids.js +20 -91
  27. package/dist/match-detection/features/bib/all-source-ids.js.map +7 -1
  28. package/dist/match-detection/features/bib/authors.js +20 -76
  29. package/dist/match-detection/features/bib/authors.js.map +7 -1
  30. package/dist/match-detection/features/bib/bibliographic-level.js +4 -40
  31. package/dist/match-detection/features/bib/bibliographic-level.js.map +7 -1
  32. package/dist/match-detection/features/bib/host-component.js +5 -41
  33. package/dist/match-detection/features/bib/host-component.js.map +7 -1
  34. package/dist/match-detection/features/bib/index.js +17 -119
  35. package/dist/match-detection/features/bib/index.js.map +7 -1
  36. package/dist/match-detection/features/bib/index.test.js +40 -0
  37. package/dist/match-detection/features/bib/index.test.js.map +7 -0
  38. package/dist/match-detection/features/bib/isbn.js +13 -69
  39. package/dist/match-detection/features/bib/isbn.js.map +7 -1
  40. package/dist/match-detection/features/bib/issn.js +5 -50
  41. package/dist/match-detection/features/bib/issn.js.map +7 -1
  42. package/dist/match-detection/features/bib/language.js +25 -86
  43. package/dist/match-detection/features/bib/language.js.map +7 -1
  44. package/dist/match-detection/features/bib/media-type.js +12 -64
  45. package/dist/match-detection/features/bib/media-type.js.map +7 -1
  46. package/dist/match-detection/features/bib/melinda-id.js +5 -47
  47. package/dist/match-detection/features/bib/melinda-id.js.map +7 -1
  48. package/dist/match-detection/features/bib/melinda-identifier-factory.js +16 -67
  49. package/dist/match-detection/features/bib/melinda-identifier-factory.js.map +7 -1
  50. package/dist/match-detection/features/bib/other-standard-identifier.js +5 -51
  51. package/dist/match-detection/features/bib/other-standard-identifier.js.map +7 -1
  52. package/dist/match-detection/features/bib/publication-time-allow-cons-years-multi.js +49 -189
  53. package/dist/match-detection/features/bib/publication-time-allow-cons-years-multi.js.map +7 -1
  54. package/dist/match-detection/features/bib/publication-time-allow-cons-years.js +10 -53
  55. package/dist/match-detection/features/bib/publication-time-allow-cons-years.js.map +7 -1
  56. package/dist/match-detection/features/bib/publication-time.js +8 -46
  57. package/dist/match-detection/features/bib/publication-time.js.map +7 -1
  58. package/dist/match-detection/features/bib/record-type.js +4 -43
  59. package/dist/match-detection/features/bib/record-type.js.map +7 -1
  60. package/dist/match-detection/features/bib/standard-identifier-factory.js +47 -163
  61. package/dist/match-detection/features/bib/standard-identifier-factory.js.map +7 -1
  62. package/dist/match-detection/features/bib/title-version-original.js +11 -53
  63. package/dist/match-detection/features/bib/title-version-original.js.map +7 -1
  64. package/dist/match-detection/features/bib/title.js +20 -72
  65. package/dist/match-detection/features/bib/title.js.map +7 -1
  66. package/dist/match-detection/features/index.js +3 -11
  67. package/dist/match-detection/features/index.js.map +7 -1
  68. package/dist/match-detection/index.js +35 -150
  69. package/dist/match-detection/index.js.map +7 -1
  70. package/dist/match-detection/index.test.js +44 -0
  71. package/dist/match-detection/index.test.js.map +7 -0
  72. package/dist/matching-utils.js +38 -102
  73. package/dist/matching-utils.js.map +7 -1
  74. package/eslint.config.js +52 -0
  75. package/example.env +4 -0
  76. package/package.json +29 -78
  77. package/src/candidate-search/candidate-search-utils.js +1 -28
  78. package/src/candidate-search/choose-queries.js +93 -0
  79. package/src/candidate-search/index.js +85 -119
  80. package/src/candidate-search/{index.spec.js → index.test.js} +30 -43
  81. package/src/candidate-search/query-list/bib.js +54 -55
  82. package/src/candidate-search/query-list/bib.test.js +38 -0
  83. package/src/candidate-search/query-list/component.js +127 -0
  84. package/src/candidate-search/query-list/index.js +31 -35
  85. package/src/cli.js +177 -0
  86. package/src/index.js +27 -68
  87. package/src/{index.spec.js → index.test.js} +11 -42
  88. package/src/match-detection/features/bib/all-source-ids.js +2 -29
  89. package/src/match-detection/features/bib/authors.js +5 -30
  90. package/src/match-detection/features/bib/bibliographic-level.js +0 -27
  91. package/src/match-detection/features/bib/host-component.js +0 -27
  92. package/src/match-detection/features/bib/index.js +16 -43
  93. package/src/match-detection/features/bib/index.test.js +52 -0
  94. package/src/match-detection/features/bib/isbn.js +2 -29
  95. package/src/match-detection/features/bib/issn.js +1 -28
  96. package/src/match-detection/features/bib/language.js +5 -32
  97. package/src/match-detection/features/bib/media-type.js +2 -28
  98. package/src/match-detection/features/bib/melinda-id.js +1 -28
  99. package/src/match-detection/features/bib/melinda-identifier-factory.js +5 -32
  100. package/src/match-detection/features/bib/other-standard-identifier.js +1 -28
  101. package/src/match-detection/features/bib/publication-time-allow-cons-years-multi.js +7 -34
  102. package/src/match-detection/features/bib/publication-time-allow-cons-years.js +1 -28
  103. package/src/match-detection/features/bib/publication-time.js +2 -28
  104. package/src/match-detection/features/bib/record-type.js +0 -27
  105. package/src/match-detection/features/bib/standard-identifier-factory.js +1 -28
  106. package/src/match-detection/features/bib/title-version-original.js +2 -28
  107. package/src/match-detection/features/bib/title.js +6 -34
  108. package/src/match-detection/features/index.js +1 -28
  109. package/src/match-detection/index.js +1 -28
  110. package/src/match-detection/{index.spec.js → index.test.js} +9 -35
  111. package/src/matching-utils.js +8 -37
  112. package/dist/candidate-search/index.spec.js +0 -144
  113. package/dist/candidate-search/index.spec.js.map +0 -1
  114. package/dist/candidate-search/query-list/bib.spec.js +0 -64
  115. package/dist/candidate-search/query-list/bib.spec.js.map +0 -1
  116. package/dist/index.spec.js +0 -127
  117. package/dist/index.spec.js.map +0 -1
  118. package/dist/match-detection/features/bib/index.spec.js +0 -93
  119. package/dist/match-detection/features/bib/index.spec.js.map +0 -1
  120. package/dist/match-detection/index.spec.js +0 -91
  121. package/dist/match-detection/index.spec.js.map +0 -1
  122. package/src/candidate-search/query-list/bib.spec.js +0 -54
  123. package/src/match-detection/features/bib/index.spec.js +0 -78
package/src/cli.js ADDED
@@ -0,0 +1,177 @@
1
+ import fs from 'fs';
2
+ import yargs from 'yargs';
3
+ import createMatchOperator, {candidateSearch, matchDetection} from './index.js';
4
+ import createDebugLogger from 'debug';
5
+ import {MarcRecord} from '@natlibfi/marc-record';
6
+
7
+ cli();
8
+
9
+ async function cli() {
10
+ const debug = createDebugLogger('@natlibfi/melinda-record-matching:cli');
11
+ const args = yargs(process.argv.slice(2))
12
+ .scriptName('melinda-record-matching-js')
13
+ .epilog('Copyright (C) 2022-2023 University Of Helsinki (The National Library Of Finland)')
14
+ .usage('$0 <file> [options] and env variable info in README')
15
+ .usage('Installed globally: $0 <file> [options] and env variable info in README')
16
+ .usage('Not installed: npx $0 <file> [options] and env variable info in README')
17
+ .usage('Build from source: node dist/index.js <file> [options] and env variable info in README')
18
+ .showHelpOnFail(true)
19
+ .example([['$ node /dist/cli.js record.json']])
20
+ .env('MELINDA_RECORD_MATCH')
21
+ .version()
22
+ .positional('file', {type: 'string', describe: 'Json file of records to match'})
23
+ .options({
24
+ t: {type: 'string', default: 'IDS', alias: 'searchType', describe: 'IDS, STANDARD_IDS, COMPONENT, CONTENT or CONTENTALT'},
25
+ m: {type: 'number', default: 1, alias: 'maxMatches', describe: ''},
26
+ c: {type: 'number', default: 1000, alias: 'maxCandidates', describe: ''},
27
+ s: {type: 'boolean', default: false, alias: 'returnStrategy', describe: ''},
28
+ q: {type: 'boolean', default: false, alias: 'returnQuery', describe: ''},
29
+ n: {type: 'boolean', default: false, alias: 'returnNonMatches', describe: ''}
30
+ })
31
+ .check((args) => {
32
+ const [file] = args._;
33
+ if (file === undefined) {
34
+ throw new Error('No file argument given');
35
+ }
36
+
37
+ if (!fs.existsSync(file)) {
38
+ throw new Error(`File ${file} does not exist`);
39
+ }
40
+
41
+ if (args.sruUrl === undefined) {
42
+ throw new Error('Setup sru url');
43
+ }
44
+
45
+ if (!['IDS', 'STANDARD_IDS', 'COMPONENT', 'CONTENT', 'CONTENTALT'].includes(args.searchType)) {
46
+ throw new Error('Invalid search type');
47
+ }
48
+
49
+ return true;
50
+ })
51
+ .parseSync();
52
+
53
+ const [file] = args._;
54
+ const {searchType} = args;
55
+ debug(JSON.stringify(args));
56
+
57
+ const detection = {
58
+ treshold: 0.9,
59
+ strategy: generateStrategy(searchType)
60
+ };
61
+
62
+ const search = {
63
+ url: args.sruUrl, searchSpec: generateSearchSpec(searchType)
64
+ };
65
+
66
+ const matchOperator = await createMatchOperator({detection, search, ...args});
67
+
68
+ const fileRaw = fs.readFileSync(file, 'utf8');
69
+ const record = new MarcRecord(JSON.parse(fileRaw), {subfieldValues: false});
70
+
71
+ const result = await matchOperator({record});
72
+ debug(JSON.stringify(result));
73
+
74
+
75
+ function generateStrategy(searchType) {
76
+ if (['IDS'].includes(searchType)) {
77
+ return [
78
+ matchDetection.features.bib.melindaId(),
79
+ matchDetection.features.bib.allSourceIds()
80
+ ];
81
+ }
82
+
83
+ // We could have differing strategy for STANDARD_IDS
84
+ // Let's not run title in strategy when we found the candidates through standard_ids search
85
+
86
+ if (['STANDARD_IDS'].includes(searchType)) {
87
+ return [
88
+ matchDetection.features.bib.hostComponent(),
89
+ matchDetection.features.bib.isbn(),
90
+ matchDetection.features.bib.issn(),
91
+ matchDetection.features.bib.otherStandardIdentifier(),
92
+ // Let's not use the same title matchDetection here
93
+ //matchDetection.features.bib.title(),
94
+ matchDetection.features.bib.authors(),
95
+ // We probably should have some leeway here for notated music as BK etc.
96
+ matchDetection.features.bib.recordType(),
97
+ // Use publicationTimeAllowConsYearsMulti to
98
+ // - ignore one year differences in publicationTime
99
+ // - extract publicationTimes from f008, f26x and reprint notes in f500
100
+ // - do not substract points for mismatching (normal) publicationTime, if there's a match between
101
+ // normal publicationTime and a reprintPublication time
102
+ matchDetection.features.bib.publicationTimeAllowConsYearsMulti(),
103
+ matchDetection.features.bib.language(),
104
+ matchDetection.features.bib.bibliographicLevel()
105
+ ];
106
+ }
107
+
108
+ if (['COMPONENT'].includes(searchType)) {
109
+ return [
110
+ matchDetection.features.bib.hostComponent(),
111
+ matchDetection.features.bib.otherStandardIdentifier(),
112
+ matchDetection.features.bib.recordType(),
113
+ matchDetection.features.bib.title(),
114
+ matchDetection.features.bib.language(),
115
+ matchDetection.features.bib.authors(),
116
+ matchDetection.features.bib.bibliographicLevel()
117
+ ];
118
+ }
119
+
120
+ if (['CONTENT', 'CONTENTALT'].includes(searchType)) {
121
+ return [
122
+ matchDetection.features.bib.hostComponent(),
123
+ matchDetection.features.bib.isbn(),
124
+ matchDetection.features.bib.issn(),
125
+ matchDetection.features.bib.otherStandardIdentifier(),
126
+ matchDetection.features.bib.title(),
127
+ matchDetection.features.bib.authors(),
128
+ matchDetection.features.bib.recordType(),
129
+ matchDetection.features.bib.publicationTime(),
130
+ matchDetection.features.bib.language(),
131
+ matchDetection.features.bib.bibliographicLevel()
132
+ ];
133
+ }
134
+
135
+ throw new Error('Unsupported match validation package');
136
+ }
137
+
138
+
139
+ function generateSearchSpec(searchType) {
140
+ if (['IDS'].includes(searchType)) {
141
+ return [
142
+ candidateSearch.searchTypes.bib.melindaId,
143
+ candidateSearch.searchTypes.bib.sourceIds
144
+ ];
145
+ }
146
+
147
+ if (['STANDARD_IDS'].includes(searchType)) {
148
+ return [candidateSearch.searchTypes.bib.standardIdentifiers];
149
+ }
150
+
151
+ if (['COMPONENT'].includes(searchType)) {
152
+ return [
153
+ //candidateSearch.searchTypes.bib.sourceIds,
154
+ candidateSearch.searchTypes.component.hostIdMelinda,
155
+ candidateSearch.searchTypes.component.hostIdOtherSource
156
+ ];
157
+ }
158
+
159
+ if (['CONTENT'].includes(searchType)) {
160
+ return [
161
+ candidateSearch.searchTypes.bib.hostComponents,
162
+ //candidateSearch.searchTypes.bib.titleAuthor,
163
+ candidateSearch.searchTypes.bib.title
164
+ ];
165
+ }
166
+ if (['CONTENTALT'].includes(searchType)) {
167
+ return [
168
+ candidateSearch.searchTypes.bib.hostComponents,
169
+ // titleAuthorYearAlternates searches for matchCandidates
170
+ // with alternate queries, starting from more tight searches
171
+ candidateSearch.searchTypes.bib.titleAuthorYearAlternates
172
+ ];
173
+ }
174
+
175
+ throw new Error('Unsupported match validation package');
176
+ }
177
+ }
package/src/index.js CHANGED
@@ -1,39 +1,11 @@
1
- /**
2
- *
3
- * @licstart The following is the entire license notice for the JavaScript code in this file.
4
- *
5
- * Melinda record matching modules for Javascript
6
- *
7
- * Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)
8
- *
9
- * This file is part of melinda-record-matching-js
10
- *
11
- * melinda-record-matching-js program is free software: you can redistribute it and/or modify
12
- * it under the terms of the GNU Lesser General Public License as
13
- * published by the Free Software Foundation, either version 3 of the
14
- * License, or (at your option) any later version.
15
- *
16
- * melinda-record-matching-js is distributed in the hope that it will be useful,
17
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
- * GNU Lesser General Public License for more details.
20
- *
21
- * You should have received a copy of the GNU Affero General Public License
22
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
- *
24
- * @licend The above is the entire license notice
25
- * for the JavaScript code in this file.
26
- *
27
- */
28
-
29
1
  import createDebugLogger from 'debug';
30
- import createSearchInterface, * as candidateSearch from './candidate-search';
31
- import createDetectionInterface, * as matchDetection from './match-detection';
2
+ import createSearchInterface, * as candidateSearch from './candidate-search/index.js';
3
+ import createDetectionInterface, * as matchDetection from './match-detection/index.js';
32
4
  //import inspect from 'util';
33
5
 
34
6
  export {candidateSearch, matchDetection};
35
7
 
36
- export default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false, returnFailures = false}) => {
8
+ export default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false}) => {
37
9
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:index');
38
10
  const debugData = debug.extend('data');
39
11
 
@@ -44,14 +16,15 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
44
16
  debugData(`ReturnStrategy: ${JSON.stringify(returnStrategy)}`);
45
17
  debugData(`ReturnQuery: ${JSON.stringify(returnQuery)}`);
46
18
  debugData(`ReturnNonMatches: ${JSON.stringify(returnNonMatches)}`);
47
- debugData(`ReturnFailures: ${JSON.stringify(returnFailures)}`);
48
19
 
49
20
 
50
21
  const detect = createDetectionInterface(detectionOptions, returnStrategy);
51
22
 
52
- return ({record, recordExternal = {recordSource: 'incomingRecord', label: 'ic'}}) => {
23
+ return prepareSearch;
24
+
25
+ async function prepareSearch({record, recordExternal = {recordSource: 'incomingRecord', label: 'ic'}}) {
53
26
 
54
- const search = createSearchInterface({...searchOptions, record, maxCandidates, recordExternal});
27
+ const {search} = await createSearchInterface({...searchOptions, record, maxCandidates, recordExternal});
55
28
  return iterate({});
56
29
 
57
30
  // candidateCount : amount of candidate records retrived from SRU for matching, NOT including current record set
@@ -67,17 +40,14 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
67
40
  // state.queryCounter : sequence for current query
68
41
  // state.maxedQueries : queries that resulted in more than serverMaxResults hits
69
42
 
70
- async function iterate({initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0, nonMatchCount = 0, conversionFailures = [], matchErrors = []}) {
43
+ async function iterate({initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0, nonMatchCount = 0, matchErrors = []}) {
71
44
  debugData(`Starting next matcher iteration.`);
72
- const {records, failures, ...state} = await search(initialState);
45
+ const {records, ...state} = await search(initialState);
73
46
 
74
- debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, nonMatchCount: ${nonMatchCount}, conversionFailures: ${conversionFailures}, matchErrors: ${matchErrors.length}`);
47
+ debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, nonMatchCount: ${nonMatchCount}, matchErrors: ${matchErrors.length}`);
75
48
  const recordSetSize = records.length;
76
- const failureSetSize = failures.length;
77
- const newCandidateCount = candidateCount + recordSetSize + failureSetSize;
49
+ const newCandidateCount = candidateCount + recordSetSize;
78
50
 
79
- const newConversionFailures = conversionFailures.concat(failures);
80
- debugData(`Failures: ${failures.length}, ConversionFailures: ${conversionFailures.length}, NewConversionFailures: ${newConversionFailures.length}`);
81
51
 
82
52
  if (recordSetSize > 0) {
83
53
  return handleRecordSet();
@@ -85,11 +55,11 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
85
55
 
86
56
  if (state.queriesLeft > 0) {
87
57
  debug(`Empty record set ${state.searchCounter} for ${state.query}, but there are ${state.queriesLeft} queries left`);
88
- return iterate({initialState: state, matches, candidateCount: newCandidateCount, nonMatches, nonMatchCount, duplicateCount, conversionFailures: newConversionFailures, matchErrors});
58
+ return iterate({initialState: state, matches, candidateCount: newCandidateCount, nonMatches, nonMatchCount, duplicateCount, matchErrors});
89
59
  }
90
60
 
91
61
  debug(`No (more) candidate records to check, no more queries left, matches: ${matches.length}`);
92
- return returnResult({matches, state, stopReason: '', nonMatches, nonMatchCount, candidateCount: newCandidateCount, duplicateCount, conversionFailures: newConversionFailures, matchErrors});
62
+ return returnResult({matches, state, stopReason: '', nonMatches, nonMatchCount, candidateCount: newCandidateCount, duplicateCount, matchErrors});
93
63
 
94
64
  function handleRecordSet() {
95
65
  debug(`Checking record set of ${recordSetSize} candidate records for possible matches, found by ${state.searchCounter} search for ${state.query}`);
@@ -101,19 +71,18 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
101
71
  const {newMatches, newNonMatches, newMatchErrors} = handleMatchResult(matchResult, matches, nonMatches, matchErrors);
102
72
 
103
73
  if (maxMatchesFound({matches: newMatches, maxMatches})) {
104
- return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures, matchErrors: newMatchErrors});
74
+ return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});
105
75
  }
106
76
 
107
77
  if (maxCandidatesRetrieved(newCandidateCount, maxCandidates)) {
108
- return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures, matchErrors: newMatchErrors});
78
+ return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});
109
79
  }
110
80
 
111
- return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount, conversionFailures: newConversionFailures, matchErrors: newMatchErrors});
81
+ return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});
112
82
  }
113
83
 
114
84
  function handleMatchResult(matchResult, matches, nonMatches, matchErrors) {
115
85
  debugData(`- Amount of new matches from record set: ${matchResult.matches.length}`);
116
- // eslint-disable-next-line functional/no-conditional-statements
117
86
  if (returnNonMatches) {
118
87
  debugData(`- Amount of new nonMatches from record set: ${matchResult.nonMatches.length}`);
119
88
  }
@@ -123,7 +92,6 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
123
92
  const newMatchErrors = matchErrors.concat(matchResult.matchErrors);
124
93
 
125
94
  debugData(`- Total amount of matches: ${newMatches.length}`);
126
- // eslint-disable-next-line functional/no-conditional-statements
127
95
  if (returnNonMatches) {
128
96
  debugData(`- Total amount of nonMatches: ${newNonMatches.length}`);
129
97
  }
@@ -167,27 +135,24 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
167
135
  // - only one stopReason is returned (if there would be several possible stopReasons, stopReason is picked in the above order)
168
136
  // - 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
169
137
 
170
- function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount, nonMatchCount, conversionFailures, matchErrors}) {
171
- const conversionFailureCount = conversionFailures.length;
138
+ function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount, nonMatchCount, matchErrors}) {
172
139
  const matchErrorCount = matchErrors.length;
173
- checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, conversionFailureCount, matchErrorCount});
174
- const matchStatus = getMatchState(state, stopReason, conversionFailureCount, matchErrorCount);
140
+ checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, matchErrorCount});
141
+ const matchStatus = getMatchState(state, stopReason, matchErrorCount);
175
142
  // add nonMatches to result only if returnNonMatches is 'true', otherwise nonMatches have not been gathered
176
143
  const matchesResult = returnNonMatches ? {matches, matchStatus, nonMatches, candidateCount} : {matches, matchStatus, candidateCount};
177
- const failures = [...conversionFailures, ...matchErrors];
178
- const result = returnFailures ? {...matchesResult, conversionFailures: failures} : matchesResult;
179
- debugData(`ReturnFailures ${returnFailures}`);
144
+ const result = matchesResult;
180
145
  debugData(`${JSON.stringify(result)}`);
181
146
  return result;
182
147
 
183
148
  // note that in cases where the matching has been stopped because of maxMatches checkCounts won't (in most cases) match
184
149
 
185
- function checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, conversionFailureCount, matchErrorCount}) {
150
+ function checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, matchErrorCount}) {
186
151
  const matchCount = matches.length;
187
152
  debugData(`Return nonMatches: ${returnNonMatches}`);
188
153
  const chosenNonMatchCount = returnNonMatches ? nonMatches.length : nonMatchCount;
189
154
  const totalHandled = matchCount + chosenNonMatchCount + duplicateCount;
190
- debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}, conversionFailureCount: ${conversionFailureCount}, matchErrorCount: ${matchErrorCount}`);
155
+ debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}, matchErrorCount: ${matchErrorCount}`);
191
156
  debug(`We got result for ${totalHandled} / ${candidateCount} retrieved candidates`);
192
157
  if (totalHandled !== candidateCount) {
193
158
  debug(`WARNING: Missing results for ${candidateCount - totalHandled} candidates`);
@@ -197,9 +162,8 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
197
162
  }
198
163
 
199
164
  // eslint-disable-next-line max-statements
200
- function getMatchState(state, stopReason, conversionFailuresCount, matchErrorCount) {
165
+ function getMatchState(state, stopReason, matchErrorCount) {
201
166
  debugData(`${JSON.stringify(state)}`);
202
- debug(`We had ${conversionFailuresCount} retrieved candidates that could not be converted.`);
203
167
  debug(`We had ${matchErrorCount} retrieved candidates that errored in matchDetection.`);
204
168
  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}`);
205
169
 
@@ -209,16 +173,14 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
209
173
  const nonRetrieved = searchesLeft ? state.totalRecords - state.queryCandidateCounter : 0;
210
174
  debugData(`nonRetrieved: ${nonRetrieved}`);
211
175
 
212
- // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries','conversionFailures', empty string/undefined), reason for stopping retrieving or handling the candidate records
176
+ // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries', empty string/undefined), reason for stopping retrieving or handling the candidate records
213
177
  // 'maxMatches' and 'maxCandidates' are in stopReason, 'maxedQueries', 'conversionFailures' and 'matchErrors' are created here
214
178
 
215
- if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0 || conversionFailureCount > 0 || matchErrorCount > 0) {
179
+ if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0 || matchErrorCount > 0) {
216
180
  const maxedQueriesStopReason = state.maxedQueries.length > 0 ? 'maxedQueries' : undefined;
217
- const conversionFailuresStopReason = conversionFailureCount > 0 ? 'conversionFailures' : undefined;
218
181
  const matchErrorsStopReason = matchErrorCount > 0 ? 'matchErrors' : undefined;
219
- const newStopReason = stopReason === '' || stopReason === undefined ? maxedQueriesStopReason || conversionFailuresStopReason || matchErrorsStopReason : stopReason;
182
+ const newStopReason = stopReason === '' || stopReason === undefined ? maxedQueriesStopReason || matchErrorsStopReason : stopReason;
220
183
  debugData(`MaxedQueriesStopReason: <${maxedQueriesStopReason}>`);
221
- debugData(`ConversionFailureStopReason <${conversionFailuresStopReason}>`);
222
184
  debugData(`MatchErrorsStopReason <${matchErrorsStopReason}>`);
223
185
  debugData(`NewStopReason: <${newStopReason}>`);
224
186
  debug(`Match status: false`);
@@ -266,8 +228,6 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
266
228
  */
267
229
 
268
230
  if (candidate) {
269
-
270
- // eslint-disable-next-line functional/no-conditional-statements
271
231
  if (candidateNotInMatches(matches.concat(nonMatches), candidate)) {
272
232
  const {record: candidateRecord, id: candidateId} = candidate;
273
233
  const recordBExternal = {id: candidate.id, recordSource: 'databaseRecord', label: `db-${candidate.id}`};
@@ -330,7 +290,6 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
330
290
 
331
291
  debugData(`- Total matches after this detection: ${matches.concat(newRecordMatches).length} (max: ${maxMatches})`);
332
292
 
333
- // eslint-disable-next-line functional/no-conditional-statements
334
293
  if (returnNonMatches) {
335
294
  debugData(`- Total nonMatches after this detection: ${nonMatches.concat(newRecordNonMatches).length}`);
336
295
  }
@@ -365,5 +324,5 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
365
324
  return true;
366
325
  }
367
326
  }
368
- };
327
+ }
369
328
  };
@@ -1,37 +1,10 @@
1
- /**
2
- *
3
- * @licstart The following is the entire license notice for the JavaScript code in this file.
4
- *
5
- * Melinda record matching modules for Javascript
6
- *
7
- * Copyright (C) 2020-2023 University Of Helsinki (The National Library Of Finland)
8
- *
9
- * This file is part of melinda-record-matching-js
10
- *
11
- * melinda-record-matching-js program is free software: you can redistribute it and/or modify
12
- * it under the terms of the GNU Lesser General Public License as
13
- * published by the Free Software Foundation, either version 3 of the
14
- * License, or (at your option) any later version.
15
- *
16
- * melinda-record-matching-js is distributed in the hope that it will be useful,
17
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
- * GNU Lesser General Public License for more details.
20
- *
21
- * You should have received a copy of the GNU Affero General Public License
22
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
- *
24
- * @licend The above is the entire license notice
25
- * for the JavaScript code in this file.
26
- *
27
- */
28
-
29
- import {expect} from 'chai';
1
+ import assert from 'node:assert';
2
+ import {describe} from 'node:test';
3
+ import createDebugLogger from 'debug';
30
4
  import {READERS} from '@natlibfi/fixura';
31
5
  import generateTests from '@natlibfi/fixugen-http-client';
32
6
  import {MarcRecord} from '@natlibfi/marc-record';
33
- import createMatchInterface, {matchDetection} from '.';
34
- import createDebugLogger from 'debug';
7
+ import createMatchInterface, {matchDetection} from './index.js';
35
8
 
36
9
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:index:test');
37
10
  const debugData = debug.extend('data');
@@ -39,7 +12,7 @@ const debugData = debug.extend('data');
39
12
  describe('INDEX', () => {
40
13
  generateTests({
41
14
  callback,
42
- path: [__dirname, '..', 'test-fixtures', 'index'],
15
+ path: [import.meta.dirname, '..', 'test-fixtures', 'index'],
43
16
  recurse: false,
44
17
  fixura: {
45
18
  reader: READERS.JSON
@@ -58,19 +31,18 @@ describe('INDEX', () => {
58
31
  const {matches, matchStatus, nonMatches, conversionFailures, candidateCount} = await match({record});
59
32
  debugData(`Matches: ${matches.length}, Status: ${matchStatus.status}/${matchStatus.stopReason}, NonMatches: ${nonMatches ? nonMatches.length : 'not returned'}, ConversionFailures: ${conversionFailures ? conversionFailures.length : 'not returned'}`);
60
33
 
61
- expect(matchStatus.status).to.eql(expectedMatchStatus);
62
- expect(matchStatus.stopReason).to.eql(expectedStopReason);
63
- expect(candidateCount).to.eql(expectedCandidateCount);
34
+ assert.equal(matchStatus.status, expectedMatchStatus);
35
+ assert.equal(matchStatus.stopReason, expectedStopReason);
36
+ assert.equal(candidateCount, expectedCandidateCount);
64
37
 
65
38
  const formattedMatchResult = formatRecordResults(matches);
66
- expect(formattedMatchResult).to.eql(expectedMatches);
39
+ assert.deepStrictEqual(formattedMatchResult, expectedMatches);
67
40
 
68
41
  const formattedNonMatchResult = formatRecordResults(nonMatches);
69
- expect(formattedNonMatchResult).to.eql(expectedNonMatches);
42
+ assert.deepStrictEqual(formattedNonMatchResult, expectedNonMatches);
70
43
 
71
- // eslint-disable-next-line functional/no-conditional-statements
72
44
  if (expectedFailures) {
73
- expect(conversionFailures).to.eql(expectedFailures);
45
+ assert.deepStrictEqual(conversionFailures, expectedFailures);
74
46
  }
75
47
 
76
48
  function formatOptions() {
@@ -108,8 +80,5 @@ describe('INDEX', () => {
108
80
  record: newRecord.toObject()
109
81
  };
110
82
  }
111
-
112
-
113
83
  }
114
-
115
84
  });
@@ -1,33 +1,6 @@
1
- /**
2
- *
3
- * @licstart The following is the entire license notice for the JavaScript code in this file.
4
- *
5
- * Melinda record matching modules for Javascript
6
- *
7
- * Copyright (C) 2020 University Of Helsinki (The National Library Of Finland)
8
- *
9
- * This file is part of melinda-record-matching-js
10
- *
11
- * melinda-record-matching-js program is free software: you can redistribute it and/or modify
12
- * it under the terms of the GNU Lesser General Public License as
13
- * published by the Free Software Foundation, either version 3 of the
14
- * License, or (at your option) any later version.
15
- *
16
- * melinda-record-matching-js is distributed in the hope that it will be useful,
17
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
- * GNU Lesser General Public License for more details.
20
- *
21
- * You should have received a copy of the GNU Affero General Public License
22
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
- *
24
- * @licend The above is the entire license notice
25
- * for the JavaScript code in this file.
26
- *
27
- */
28
1
 
29
2
  import createDebugLogger from 'debug';
30
- import {validateSidFieldSubfieldCounts, getSubfieldValues} from '../../../matching-utils';
3
+ import {validateSidFieldSubfieldCounts, getSubfieldValues} from '../../../matching-utils.js';
31
4
 
32
5
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:all-source-ids');
33
6
  const debugData = debug.extend('data');
@@ -98,7 +71,7 @@ export default () => ({
98
71
  debugData(`Comparing ${JSON.stringify(a)} and ${JSON.stringify(b)}`);
99
72
 
100
73
  const matches = a.filter(sidA => b.some(sidB => sidA.sourceDb === sidB.sourceDb &&
101
- sidA.sourceId === sidB.sourceId));
74
+ sidA.sourceId === sidB.sourceId));
102
75
  debugData(`Matches (${matches.length}): ${JSON.stringify(matches)}`);
103
76
 
104
77
  const mismatches = a.filter(sidA => b.some(sidB => sidA.sourceDb === sidB.sourceDb &&
@@ -1,33 +1,8 @@
1
- /**
2
- *
3
- * @licstart The following is the entire license notice for the JavaScript code in this file.
4
- *
5
- * Melinda record matching modules for Javascript
6
- *
7
- * Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)
8
- *
9
- * This file is part of melinda-record-matching-js
10
- *
11
- * melinda-record-matching-js program is free software: you can redistribute it and/or modify
12
- * it under the terms of the GNU Lesser General Public License as
13
- * published by the Free Software Foundation, either version 3 of the
14
- * License, or (at your option) any later version.
15
- *
16
- * melinda-record-matching-js is distributed in the hope that it will be useful,
17
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
- * GNU Lesser General Public License for more details.
20
- *
21
- * You should have received a copy of the GNU Affero General Public License
22
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
- *
24
- * @licend The above is the entire license notice
25
- * for the JavaScript code in this file.
26
- *
27
- */
28
-
29
- import {LevenshteinDistance as leven} from 'natural';
30
- import {testStringOrNumber} from '../../../matching-utils';
1
+
2
+
3
+ import naturalPkg from 'natural';
4
+ const {LevenshteinDistance: leven} = naturalPkg;
5
+ import {testStringOrNumber} from '../../../matching-utils.js';
31
6
 
32
7
  // We should extract also organisational authors (110/710)
33
8
 
@@ -1,30 +1,3 @@
1
- /**
2
- *
3
- * @licstart The following is the entire license notice for the JavaScript code in this file.
4
- *
5
- * Melinda record matching modules for Javascript
6
- *
7
- * Copyright (C) 2020 University Of Helsinki (The National Library Of Finland)
8
- *
9
- * This file is part of melinda-record-matching-js
10
- *
11
- * melinda-record-matching-js program is free software: you can redistribute it and/or modify
12
- * it under the terms of the GNU Lesser General Public License as
13
- * published by the Free Software Foundation, either version 3 of the
14
- * License, or (at your option) any later version.
15
- *
16
- * melinda-record-matching-js is distributed in the hope that it will be useful,
17
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
- * GNU Lesser General Public License for more details.
20
- *
21
- * You should have received a copy of the GNU Affero General Public License
22
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
- *
24
- * @licend The above is the entire license notice
25
- * for the JavaScript code in this file.
26
- *
27
- */
28
1
 
29
2
  export default () => ({
30
3
  name: 'Bibliographic level',
@@ -1,30 +1,3 @@
1
- /**
2
- *
3
- * @licstart The following is the entire license notice for the JavaScript code in this file.
4
- *
5
- * Melinda record matching modules for Javascript
6
- *
7
- * Copyright (C) 2020 University Of Helsinki (The National Library Of Finland)
8
- *
9
- * This file is part of melinda-record-matching-js
10
- *
11
- * melinda-record-matching-js program is free software: you can redistribute it and/or modify
12
- * it under the terms of the GNU Lesser General Public License as
13
- * published by the Free Software Foundation, either version 3 of the
14
- * License, or (at your option) any later version.
15
- *
16
- * melinda-record-matching-js is distributed in the hope that it will be useful,
17
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
- * GNU Lesser General Public License for more details.
20
- *
21
- * You should have received a copy of the GNU Affero General Public License
22
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
- *
24
- * @licend The above is the entire license notice
25
- * for the JavaScript code in this file.
26
- *
27
- */
28
1
 
29
2
  export default () => ({
30
3
  name: 'Host/Component record',