@natlibfi/melinda-record-matching 2.0.1-alpha.1 → 2.0.7-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.
Files changed (96) hide show
  1. package/.github/CODEOWNERS +2 -0
  2. package/.github/dependabot.yml +40 -0
  3. package/.github/workflows/melinda-node-tests.yml +70 -0
  4. package/README.md +1 -1
  5. package/dist/candidate-search/candidate-search-utils.js +3 -6
  6. package/dist/candidate-search/candidate-search-utils.js.map +1 -1
  7. package/dist/candidate-search/index.js +115 -50
  8. package/dist/candidate-search/index.js.map +1 -1
  9. package/dist/candidate-search/index.spec.js +41 -48
  10. package/dist/candidate-search/index.spec.js.map +1 -1
  11. package/dist/candidate-search/query-list/bib.js +154 -64
  12. package/dist/candidate-search/query-list/bib.js.map +1 -1
  13. package/dist/candidate-search/query-list/bib.spec.js +1 -10
  14. package/dist/candidate-search/query-list/bib.spec.js.map +1 -1
  15. package/dist/candidate-search/query-list/index.js +4 -10
  16. package/dist/candidate-search/query-list/index.js.map +1 -1
  17. package/dist/index.js +498 -50
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.spec.js +60 -34
  20. package/dist/index.spec.js.map +1 -1
  21. package/dist/match-detection/features/bib/all-source-ids.js +16 -19
  22. package/dist/match-detection/features/bib/all-source-ids.js.map +1 -1
  23. package/dist/match-detection/features/bib/authors.js +31 -35
  24. package/dist/match-detection/features/bib/authors.js.map +1 -1
  25. package/dist/match-detection/features/bib/bibliographic-level.js +3 -3
  26. package/dist/match-detection/features/bib/bibliographic-level.js.map +1 -1
  27. package/dist/match-detection/features/bib/host-component.js +3 -3
  28. package/dist/match-detection/features/bib/host-component.js.map +1 -1
  29. package/dist/match-detection/features/bib/index.js +28 -13
  30. package/dist/match-detection/features/bib/index.js.map +1 -1
  31. package/dist/match-detection/features/bib/index.spec.js +9 -13
  32. package/dist/match-detection/features/bib/index.spec.js.map +1 -1
  33. package/dist/match-detection/features/bib/isbn.js +28 -6
  34. package/dist/match-detection/features/bib/isbn.js.map +1 -1
  35. package/dist/match-detection/features/bib/issn.js +0 -4
  36. package/dist/match-detection/features/bib/issn.js.map +1 -1
  37. package/dist/match-detection/features/bib/language.js +69 -41
  38. package/dist/match-detection/features/bib/language.js.map +1 -1
  39. package/dist/match-detection/features/bib/media-type.js +83 -0
  40. package/dist/match-detection/features/bib/media-type.js.map +1 -0
  41. package/dist/match-detection/features/bib/melinda-id.js +0 -4
  42. package/dist/match-detection/features/bib/melinda-id.js.map +1 -1
  43. package/dist/match-detection/features/bib/melinda-identifier-factory.js +6 -17
  44. package/dist/match-detection/features/bib/melinda-identifier-factory.js.map +1 -1
  45. package/dist/match-detection/features/bib/other-standard-identifier.js +1 -4
  46. package/dist/match-detection/features/bib/other-standard-identifier.js.map +1 -1
  47. package/dist/match-detection/features/bib/publication-time-allow-cons-years-multi.js +256 -0
  48. package/dist/match-detection/features/bib/publication-time-allow-cons-years-multi.js.map +1 -0
  49. package/dist/match-detection/features/bib/publication-time-allow-cons-years.js +71 -0
  50. package/dist/match-detection/features/bib/publication-time-allow-cons-years.js.map +1 -0
  51. package/dist/match-detection/features/bib/publication-time.js +9 -8
  52. package/dist/match-detection/features/bib/publication-time.js.map +1 -1
  53. package/dist/match-detection/features/bib/record-type.js +6 -3
  54. package/dist/match-detection/features/bib/record-type.js.map +1 -1
  55. package/dist/match-detection/features/bib/standard-identifier-factory.js +129 -39
  56. package/dist/match-detection/features/bib/standard-identifier-factory.js.map +1 -1
  57. package/dist/match-detection/features/bib/title-version-original.js +79 -0
  58. package/dist/match-detection/features/bib/title-version-original.js.map +1 -0
  59. package/dist/match-detection/features/bib/title.js +29 -22
  60. package/dist/match-detection/features/bib/title.js.map +1 -1
  61. package/dist/match-detection/features/index.js +0 -4
  62. package/dist/match-detection/features/index.js.map +1 -1
  63. package/dist/match-detection/index.js +94 -47
  64. package/dist/match-detection/index.js.map +1 -1
  65. package/dist/match-detection/index.spec.js +25 -15
  66. package/dist/match-detection/index.spec.js.map +1 -1
  67. package/dist/matching-utils.js +63 -15
  68. package/dist/matching-utils.js.map +1 -1
  69. package/package.json +27 -24
  70. package/src/candidate-search/index.js +93 -31
  71. package/src/candidate-search/index.spec.js +27 -24
  72. package/src/candidate-search/query-list/bib.js +149 -22
  73. package/src/candidate-search/query-list/index.js +1 -1
  74. package/src/index.js +310 -38
  75. package/src/index.spec.js +49 -21
  76. package/src/match-detection/features/bib/all-source-ids.js +3 -3
  77. package/src/match-detection/features/bib/authors.js +23 -16
  78. package/src/match-detection/features/bib/bibliographic-level.js +1 -1
  79. package/src/match-detection/features/bib/host-component.js +1 -1
  80. package/src/match-detection/features/bib/index.js +4 -0
  81. package/src/match-detection/features/bib/index.spec.js +10 -2
  82. package/src/match-detection/features/bib/isbn.js +21 -2
  83. package/src/match-detection/features/bib/language.js +63 -22
  84. package/src/match-detection/features/bib/media-type.js +77 -0
  85. package/src/match-detection/features/bib/melinda-identifier-factory.js +2 -3
  86. package/src/match-detection/features/bib/other-standard-identifier.js +2 -0
  87. package/src/match-detection/features/bib/publication-time-allow-cons-years-multi.js +268 -0
  88. package/src/match-detection/features/bib/publication-time-allow-cons-years.js +67 -0
  89. package/src/match-detection/features/bib/publication-time.js +9 -4
  90. package/src/match-detection/features/bib/record-type.js +5 -1
  91. package/src/match-detection/features/bib/standard-identifier-factory.js +107 -22
  92. package/src/match-detection/features/bib/title-version-original.js +78 -0
  93. package/src/match-detection/features/bib/title.js +27 -7
  94. package/src/match-detection/index.js +60 -22
  95. package/src/match-detection/index.spec.js +18 -4
  96. package/src/matching-utils.js +62 -5
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Melinda record matching modules for Javascript
7
7
  *
8
- * Copyright (C) 2020 University Of Helsinki (The National Library Of Finland)
8
+ * Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)
9
9
  *
10
10
  * This file is part of melinda-record-matching-js
11
11
  *
@@ -28,7 +28,7 @@
28
28
  */
29
29
  import createDebugLogger from 'debug';
30
30
  import {toQueries} from '../candidate-search-utils';
31
- import {getMelindaIdsF035, validateSidFieldSubfieldCounts, getSubfieldValues} from '../../matching-utils';
31
+ import {getMelindaIdsF035, validateSidFieldSubfieldCounts, getSubfieldValues, testStringOrNumber} from '../../matching-utils';
32
32
 
33
33
 
34
34
  export function bibSourceIds(record) {
@@ -108,7 +108,7 @@ export function bibSourceIds(record) {
108
108
 
109
109
  function removeSourcePrefix(subfieldValue) {
110
110
  const sourcePrefixRegex = (/^(?<sourcePrefix>\([A-Za-z0-9-]+\))(?<id>.+)$/u);
111
- const normalizedValue = subfieldValue.replace(sourcePrefixRegex, '$<id>');
111
+ const normalizedValue = testStringOrNumber(subfieldValue) ? String(subfieldValue).replace(sourcePrefixRegex, '$<id>') : '';
112
112
  debugData(`Normalized ${subfieldValue} to ${normalizedValue}`);
113
113
  return normalizedValue;
114
114
  }
@@ -116,7 +116,7 @@ export function bibSourceIds(record) {
116
116
  function normalizeSidSubfieldValue(subfieldValue) {
117
117
  debugData(`Normalizing ${subfieldValue}`);
118
118
  const normalizeAwayRegex = (/[- ]/u);
119
- return subfieldValue.replace(normalizeAwayRegex, '');
119
+ return testStringOrNumber(subfieldValue) ? String(subfieldValue).replace(normalizeAwayRegex, '') : '';
120
120
  }
121
121
 
122
122
  }
@@ -151,7 +151,7 @@ export function bibMelindaIds(record) {
151
151
  // bibHostComponents should search all 773 $ws for possible host id, but what should it do in case of multiple host ids?
152
152
  export function bibHostComponents(record) {
153
153
  const id = getHostId();
154
- return id ? [`melinda.partsofhost=${id}`] : [];
154
+ return testStringOrNumber(id) ? [`melinda.partsofhost=${id}`] : [];
155
155
 
156
156
  function getHostId() {
157
157
  const [field] = record.get(/^773$/u);
@@ -159,12 +159,12 @@ export function bibHostComponents(record) {
159
159
  if (field) {
160
160
  const {value} = field.subfields.find(({code}) => code === 'w') || {};
161
161
 
162
- if (value && (/^\(FI-MELINDA\)/u).test(value)) {
163
- return value.replace(/^\(FI-MELINDA\)/u, '');
162
+ if (testStringOrNumber(value) && (/^\(FI-MELINDA\)/u).test(String(value))) {
163
+ return String(value).replace(/^\(FI-MELINDA\)/u, '');
164
164
  }
165
165
 
166
- if (value && (/^\(FIN01\)/u).test(value)) {
167
- return value.replace(/^\(FIN01\)/u, '');
166
+ if (testStringOrNumber(value) && (/^\(FIN01\)/u).test(String(value))) {
167
+ return String(value).replace(/^\(FIN01\)/u, '');
168
168
  }
169
169
 
170
170
  return false;
@@ -175,15 +175,18 @@ 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
- if (title) {
186
- const formatted = title
188
+ if (testStringOrNumber(title)) {
189
+ const formatted = String(title)
187
190
  .replace(/[^\w\s\p{Alphabetic}]/gu, '')
188
191
  // Clean up concurrent spaces from fe. subfield changes
189
192
  .replace(/ +/gu, ' ')
@@ -191,33 +194,157 @@ export function bibTitle(record) {
191
194
  .slice(0, 30)
192
195
  .trim();
193
196
 
194
- // Prevent too many matches by having a minimum length requirement
195
- return formatted.length >= 5 ? [`dc.title="^${formatted}*"`] : [];
197
+ // use word search for titles starting with a boolean
198
+ const useWordSearch = booleanStartWords.some(word => formatted.toLowerCase().startsWith(word));
199
+ // Prevent too many matches by having a minimum length
200
+ // Note that currently this fails matching if there are no matches from previous matchers
201
+ if (formatted.length >= 5) {
202
+ return [`dc.title="${useWordSearch ? '' : '^'}${formatted}*"`];
203
+ }
204
+ // use word search without ending * also in combination searches to avoid SRU-server crashes [MRA-189]
205
+ return addAuthorsToSearch(`dc.title="${formatted}"`);
196
206
  }
197
207
 
198
208
  return [];
199
209
 
210
+ function addAuthorsToSearch(titleQuery) {
211
+ const [authorQuery] = bibAuthors(record);
212
+ if (authorQuery !== undefined) {
213
+ return [`${authorQuery} AND ${titleQuery}`];
214
+ }
215
+ return addPublisherToSearch(titleQuery);
216
+ //return [];
217
+ }
218
+
219
+ function addPublisherToSearch(query) {
220
+ const [publisherQuery] = bibPublishers(record);
221
+ if (publisherQuery !== undefined) {
222
+ return [`${publisherQuery} AND ${query}`];
223
+ }
224
+ return [];
225
+ }
226
+
200
227
  function getTitle() {
201
228
  const [field] = record.get(/^245$/u);
202
229
 
203
230
  if (field) {
204
- return field.subfields
231
+ const titleString = field.subfields
232
+ //.filter(({code}) => ['a', 'b', 'n', 'p'].includes(code))
205
233
  .filter(({code}) => ['a', 'b'].includes(code))
206
- .map(({value}) => value)
234
+ //.filter(({code}) => ['a'].includes(code))
235
+ .map(({value}) => testStringOrNumber(value) ? String(value) : '')
236
+ .filter(value => value)
207
237
  // In Melinda's index subfield separators are indexed as ' '
208
238
  .join(' ');
239
+ return titleString;
209
240
  }
210
241
  return false;
211
242
  }
212
243
  }
213
244
 
214
- // eslint-disable-next-line max-statements
245
+ export function bibAuthors(record) {
246
+ const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibAuthors');
247
+ const debugData = debug.extend('data');
248
+ debug(`Creating query for the first author`);
249
+ //debugData(record);
250
+
251
+ const author = getAuthor(record);
252
+ const booleanStartWords = ['and', 'or', 'nor', 'not'];
253
+
254
+
255
+ if (testStringOrNumber(author)) {
256
+ const formatted = String(author)
257
+ .replace(/[^\w\s\p{Alphabetic}]/gu, '')
258
+ // Clean up concurrent spaces from fe. subfield changes
259
+ .replace(/ +/gu, ' ')
260
+ .trim()
261
+ .slice(0, 30)
262
+ .trim();
263
+
264
+ // use word search for authors starting with a boolean
265
+ const useWordSearch = booleanStartWords.some(word => formatted.toLowerCase().startsWith(word));
266
+ // Prevent too many matches by having a minimum length
267
+ debugData(`Author string: ${formatted}`);
268
+ if (formatted.length >= 5) {
269
+ return [`dc.author="${useWordSearch ? '' : '^'}${formatted}*"`];
270
+ }
271
+ //if (formatted) {
272
+ // return [`dc.author="${formatted}"`];
273
+ //}
274
+ return [];
275
+ }
276
+
277
+ return [];
278
+
279
+ function getAuthor(record) {
280
+ //debugData(record);
281
+ // eslint-disable-next-line prefer-named-capture-group
282
+ const [field] = record.get(/^(100)|(110)|(111)|(700)|(710)|(711)$/u);
283
+ //debugData(field);
284
+
285
+ if (field) {
286
+ const authorString = field.subfields
287
+ .filter(({code}) => ['a'].includes(code))
288
+ .map(({value}) => testStringOrNumber(value) ? String(value) : '')
289
+ .filter(value => value)
290
+ // In Melinda's index subfield separators are indexed as ' '
291
+ .join(' ');
292
+ return authorString;
293
+ }
294
+ return false;
295
+ }
296
+ }
297
+
298
+ export function bibPublishers(record) {
299
+ const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibPublishers');
300
+ const debugData = debug.extend('data');
301
+ debug(`Creating query for the first publisher`);
302
+ //debugData(record);
303
+
304
+ const publisher = getPublisher(record);
305
+ if (testStringOrNumber(publisher)) {
306
+ const formatted = String(publisher)
307
+ .replace(/[^\w\s\p{Alphabetic}]/gu, '')
308
+ // Clean up concurrent spaces from fe. subfield changes
309
+ .replace(/ +/gu, ' ')
310
+ .trim()
311
+ .slice(0, 30)
312
+ .trim();
313
+
314
+ debugData(`Publisher string: ${formatted}`);
315
+ // use non-wildcard word search from dc.publisher
316
+ return [`dc.publisher="${formatted}"`];
317
+ }
318
+
319
+ return [];
320
+
321
+ function getPublisher(record) {
322
+ //debugData(record);
323
+ const [field] = record.get(/^(?:260)|(?:264)$/u);
324
+ //debugData(field);
325
+
326
+ if (field) {
327
+ const publisherString = field.subfields
328
+ .filter(({code}) => ['b'].includes(code))
329
+ .map(({value}) => testStringOrNumber(value) ? String(value) : '')
330
+ .filter(value => value)
331
+ // In Melinda's index subfield separators are indexed as ' '
332
+ .join(' ');
333
+ return publisherString;
334
+ }
335
+ return false;
336
+ }
337
+ }
338
+
339
+
215
340
  export function bibStandardIdentifiers(record) {
216
341
 
217
342
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibStandardIdentifiers');
218
343
  const debugData = debug.extend('data');
219
344
  debug(`Creating queries for standard identifiers`);
220
345
 
346
+ // DEVELOP: should we query also f015 and f028?
347
+
221
348
  const fields = record.get(/^(?<def>020|022|024)$/u);
222
349
  const identifiers = [].concat(...fields.map(toIdentifiers));
223
350
  const uniqueIdentifiers = [...new Set(identifiers)];
@@ -239,18 +366,18 @@ export function bibStandardIdentifiers(record) {
239
366
 
240
367
  if (tag === '022') {
241
368
  return subfields
242
- .filter(sub => ['a', 'z', 'y'].includes(sub.code) && issnIsbnReqExp.test(sub.value) && sub.value !== undefined)
243
- .map(({value}) => value);
369
+ .filter(sub => ['a', 'z', 'y'].includes(sub.code) && testStringOrNumber(sub.value) && issnIsbnReqExp.test(String(sub.value)))
370
+ .map(({value}) => String(value));
244
371
  }
245
372
 
246
373
  if (tag === '020') {
247
374
  return subfields
248
- .filter(sub => ['a', 'z'].includes(sub.code) && issnIsbnReqExp.test(sub.value) && sub.value !== undefined)
249
- .map(({value}) => value);
375
+ .filter(sub => ['a', 'z'].includes(sub.code) && testStringOrNumber(sub.value) && issnIsbnReqExp.test(String(sub.value)))
376
+ .map(({value}) => String(value));
250
377
  }
251
378
 
252
379
  return subfields
253
- .filter(sub => ['a', 'z'].includes(sub.code) && otherIdReqExp.test(sub.value) && sub.value !== undefined)
254
- .map(({value}) => value);
380
+ .filter(sub => ['a', 'z'].includes(sub.code) && testStringOrNumber(sub.value) && otherIdReqExp.test(String(sub.value)))
381
+ .map(({value}) => String(value));
255
382
  }
256
383
  }
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Melinda record matching modules for Javascript
6
6
  *
7
- * Copyright (C) 2020 University Of Helsinki (The National Library Of Finland)
7
+ * Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)
8
8
  *
9
9
  * This file is part of melinda-record-matching-js
10
10
  *