@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.
- package/.github/CODEOWNERS +2 -0
- package/.github/dependabot.yml +40 -0
- package/.github/workflows/melinda-node-tests.yml +70 -0
- package/README.md +1 -1
- package/dist/candidate-search/candidate-search-utils.js +3 -6
- package/dist/candidate-search/candidate-search-utils.js.map +1 -1
- package/dist/candidate-search/index.js +115 -50
- package/dist/candidate-search/index.js.map +1 -1
- package/dist/candidate-search/index.spec.js +41 -48
- package/dist/candidate-search/index.spec.js.map +1 -1
- package/dist/candidate-search/query-list/bib.js +154 -64
- package/dist/candidate-search/query-list/bib.js.map +1 -1
- package/dist/candidate-search/query-list/bib.spec.js +1 -10
- package/dist/candidate-search/query-list/bib.spec.js.map +1 -1
- package/dist/candidate-search/query-list/index.js +4 -10
- package/dist/candidate-search/query-list/index.js.map +1 -1
- package/dist/index.js +498 -50
- package/dist/index.js.map +1 -1
- package/dist/index.spec.js +60 -34
- package/dist/index.spec.js.map +1 -1
- package/dist/match-detection/features/bib/all-source-ids.js +16 -19
- package/dist/match-detection/features/bib/all-source-ids.js.map +1 -1
- package/dist/match-detection/features/bib/authors.js +31 -35
- package/dist/match-detection/features/bib/authors.js.map +1 -1
- package/dist/match-detection/features/bib/bibliographic-level.js +3 -3
- package/dist/match-detection/features/bib/bibliographic-level.js.map +1 -1
- package/dist/match-detection/features/bib/host-component.js +3 -3
- package/dist/match-detection/features/bib/host-component.js.map +1 -1
- package/dist/match-detection/features/bib/index.js +28 -13
- package/dist/match-detection/features/bib/index.js.map +1 -1
- package/dist/match-detection/features/bib/index.spec.js +9 -13
- package/dist/match-detection/features/bib/index.spec.js.map +1 -1
- package/dist/match-detection/features/bib/isbn.js +28 -6
- package/dist/match-detection/features/bib/isbn.js.map +1 -1
- package/dist/match-detection/features/bib/issn.js +0 -4
- package/dist/match-detection/features/bib/issn.js.map +1 -1
- package/dist/match-detection/features/bib/language.js +69 -41
- package/dist/match-detection/features/bib/language.js.map +1 -1
- package/dist/match-detection/features/bib/media-type.js +83 -0
- package/dist/match-detection/features/bib/media-type.js.map +1 -0
- package/dist/match-detection/features/bib/melinda-id.js +0 -4
- package/dist/match-detection/features/bib/melinda-id.js.map +1 -1
- package/dist/match-detection/features/bib/melinda-identifier-factory.js +6 -17
- package/dist/match-detection/features/bib/melinda-identifier-factory.js.map +1 -1
- package/dist/match-detection/features/bib/other-standard-identifier.js +1 -4
- package/dist/match-detection/features/bib/other-standard-identifier.js.map +1 -1
- package/dist/match-detection/features/bib/publication-time-allow-cons-years-multi.js +256 -0
- package/dist/match-detection/features/bib/publication-time-allow-cons-years-multi.js.map +1 -0
- package/dist/match-detection/features/bib/publication-time-allow-cons-years.js +71 -0
- package/dist/match-detection/features/bib/publication-time-allow-cons-years.js.map +1 -0
- package/dist/match-detection/features/bib/publication-time.js +9 -8
- package/dist/match-detection/features/bib/publication-time.js.map +1 -1
- package/dist/match-detection/features/bib/record-type.js +6 -3
- package/dist/match-detection/features/bib/record-type.js.map +1 -1
- package/dist/match-detection/features/bib/standard-identifier-factory.js +129 -39
- package/dist/match-detection/features/bib/standard-identifier-factory.js.map +1 -1
- package/dist/match-detection/features/bib/title-version-original.js +79 -0
- package/dist/match-detection/features/bib/title-version-original.js.map +1 -0
- package/dist/match-detection/features/bib/title.js +29 -22
- package/dist/match-detection/features/bib/title.js.map +1 -1
- package/dist/match-detection/features/index.js +0 -4
- package/dist/match-detection/features/index.js.map +1 -1
- package/dist/match-detection/index.js +94 -47
- package/dist/match-detection/index.js.map +1 -1
- package/dist/match-detection/index.spec.js +25 -15
- package/dist/match-detection/index.spec.js.map +1 -1
- package/dist/matching-utils.js +63 -15
- package/dist/matching-utils.js.map +1 -1
- package/package.json +27 -24
- package/src/candidate-search/index.js +93 -31
- package/src/candidate-search/index.spec.js +27 -24
- package/src/candidate-search/query-list/bib.js +149 -22
- package/src/candidate-search/query-list/index.js +1 -1
- package/src/index.js +310 -38
- package/src/index.spec.js +49 -21
- package/src/match-detection/features/bib/all-source-ids.js +3 -3
- package/src/match-detection/features/bib/authors.js +23 -16
- package/src/match-detection/features/bib/bibliographic-level.js +1 -1
- package/src/match-detection/features/bib/host-component.js +1 -1
- package/src/match-detection/features/bib/index.js +4 -0
- package/src/match-detection/features/bib/index.spec.js +10 -2
- package/src/match-detection/features/bib/isbn.js +21 -2
- package/src/match-detection/features/bib/language.js +63 -22
- package/src/match-detection/features/bib/media-type.js +77 -0
- package/src/match-detection/features/bib/melinda-identifier-factory.js +2 -3
- package/src/match-detection/features/bib/other-standard-identifier.js +2 -0
- package/src/match-detection/features/bib/publication-time-allow-cons-years-multi.js +268 -0
- package/src/match-detection/features/bib/publication-time-allow-cons-years.js +67 -0
- package/src/match-detection/features/bib/publication-time.js +9 -4
- package/src/match-detection/features/bib/record-type.js +5 -1
- package/src/match-detection/features/bib/standard-identifier-factory.js +107 -22
- package/src/match-detection/features/bib/title-version-original.js +78 -0
- package/src/match-detection/features/bib/title.js +27 -7
- package/src/match-detection/index.js +60 -22
- package/src/match-detection/index.spec.js +18 -4
- 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
|
-
//
|
|
195
|
-
|
|
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
|
-
|
|
231
|
+
const titleString = field.subfields
|
|
232
|
+
//.filter(({code}) => ['a', 'b', 'n', 'p'].includes(code))
|
|
205
233
|
.filter(({code}) => ['a', 'b'].includes(code))
|
|
206
|
-
|
|
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
|
-
|
|
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) &&
|
|
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) &&
|
|
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) &&
|
|
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
|
*
|