@natlibfi/melinda-record-matching 2.2.2 → 3.0.1-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 +0 -4
- package/dist/candidate-search/index.js.map +1 -1
- package/dist/candidate-search/index.spec.js +1 -5
- package/dist/candidate-search/index.spec.js.map +1 -1
- package/dist/candidate-search/query-list/bib.js +0 -2
- package/dist/candidate-search/query-list/bib.js.map +1 -1
- package/dist/index.js +20 -4
- package/dist/index.js.map +1 -1
- package/dist/index.spec.js +3 -1
- package/dist/index.spec.js.map +1 -1
- package/dist/match-detection/features/bib/all-source-ids.js +6 -3
- package/dist/match-detection/features/bib/all-source-ids.js.map +1 -1
- package/dist/match-detection/features/bib/authors.js +28 -22
- package/dist/match-detection/features/bib/authors.js.map +1 -1
- package/dist/match-detection/features/bib/bibliographic-level.js +5 -2
- package/dist/match-detection/features/bib/bibliographic-level.js.map +1 -1
- package/dist/match-detection/features/bib/host-component.js +5 -2
- package/dist/match-detection/features/bib/host-component.js.map +1 -1
- package/dist/match-detection/features/bib/index.spec.js +3 -2
- package/dist/match-detection/features/bib/index.spec.js.map +1 -1
- package/dist/match-detection/features/bib/isbn.js +2 -1
- package/dist/match-detection/features/bib/isbn.js.map +1 -1
- package/dist/match-detection/features/bib/issn.js +2 -1
- package/dist/match-detection/features/bib/issn.js.map +1 -1
- package/dist/match-detection/features/bib/language.js +12 -8
- package/dist/match-detection/features/bib/language.js.map +1 -1
- package/dist/match-detection/features/bib/melinda-id.js +2 -1
- package/dist/match-detection/features/bib/melinda-id.js.map +1 -1
- package/dist/match-detection/features/bib/melinda-identifier-factory.js +5 -4
- package/dist/match-detection/features/bib/melinda-identifier-factory.js.map +1 -1
- package/dist/match-detection/features/bib/other-standard-identifier.js +2 -1
- package/dist/match-detection/features/bib/other-standard-identifier.js.map +1 -1
- package/dist/match-detection/features/bib/publication-time.js +3 -1
- package/dist/match-detection/features/bib/publication-time.js.map +1 -1
- package/dist/match-detection/features/bib/record-type.js +3 -1
- package/dist/match-detection/features/bib/record-type.js.map +1 -1
- package/dist/match-detection/features/bib/standard-identifier-factory.js +3 -1
- package/dist/match-detection/features/bib/standard-identifier-factory.js.map +1 -1
- package/dist/match-detection/features/bib/title.js +26 -6
- package/dist/match-detection/features/bib/title.js.map +1 -1
- package/dist/match-detection/index.js +54 -15
- package/dist/match-detection/index.js.map +1 -1
- package/dist/match-detection/index.spec.js +4 -1
- package/dist/match-detection/index.spec.js.map +1 -1
- package/dist/matching-utils.js +0 -2
- package/dist/matching-utils.js.map +1 -1
- package/package.json +8 -8
- package/src/candidate-search/index.js +1 -3
- package/src/candidate-search/index.spec.js +1 -3
- package/src/candidate-search/query-list/bib.js +0 -1
- package/src/index.js +5 -4
- package/src/index.spec.js +1 -1
- package/src/match-detection/features/bib/all-source-ids.js +3 -3
- package/src/match-detection/features/bib/authors.js +19 -15
- 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.spec.js +2 -2
- package/src/match-detection/features/bib/language.js +10 -8
- package/src/match-detection/features/bib/melinda-identifier-factory.js +2 -3
- package/src/match-detection/features/bib/publication-time.js +1 -1
- package/src/match-detection/features/bib/record-type.js +1 -1
- package/src/match-detection/features/bib/standard-identifier-factory.js +1 -1
- package/src/match-detection/features/bib/title.js +22 -4
- package/src/match-detection/index.js +23 -15
- package/src/match-detection/index.spec.js +1 -1
- package/src/matching-utils.js +0 -1
|
@@ -40,7 +40,7 @@ describe('match-detection/features/bib/', () => {
|
|
|
40
40
|
fixura: {
|
|
41
41
|
reader: READERS.JSON
|
|
42
42
|
},
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
callback: ({enabled = true, feature, options, type, ...expectations}) => {
|
|
45
45
|
|
|
46
46
|
if (!enabled) {
|
|
@@ -52,7 +52,7 @@ describe('match-detection/features/bib/', () => {
|
|
|
52
52
|
const record = new MarcRecord(inputRecord, {subfieldValues: false});
|
|
53
53
|
const {extract} = features[feature](options);
|
|
54
54
|
|
|
55
|
-
expect(extract(record)).to.eql(expectedFeatures);
|
|
55
|
+
expect(extract({record})).to.eql(expectedFeatures);
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable max-statements */
|
|
2
1
|
/**
|
|
3
2
|
*
|
|
4
3
|
* @licstart The following is the entire license notice for the JavaScript code in this file.
|
|
@@ -34,20 +33,22 @@ const debugData = debug.extend('data');
|
|
|
34
33
|
|
|
35
34
|
export default () => ({
|
|
36
35
|
name: 'Language',
|
|
37
|
-
extract: record => {
|
|
36
|
+
extract: ({record, recordExternal}) => {
|
|
37
|
+
const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';
|
|
38
|
+
|
|
38
39
|
const value008 = get008Value();
|
|
39
40
|
const values041 = get041Values();
|
|
40
|
-
debugData(
|
|
41
|
+
debugData(`${label} 008: ${JSON.stringify(value008)}, 041: ${JSON.stringify(values041)}`);
|
|
41
42
|
|
|
42
43
|
if (value008 && values041.length > 0) {
|
|
43
|
-
debugData(
|
|
44
|
+
debugData(`${label} There's both 008 and 041, searching for value in both`);
|
|
44
45
|
const correspondingValue = values041.find(v => v === value008);
|
|
45
|
-
debugData(
|
|
46
|
+
debugData(`${label} Corresponding value: ${correspondingValue}`);
|
|
46
47
|
return correspondingValue ? [correspondingValue] : [];
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
if (!value008 && values041.length < 1) {
|
|
50
|
-
debugData(`No actual values found`);
|
|
51
|
+
debugData(`{$label} No actual values found`);
|
|
51
52
|
return [];
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -55,18 +56,19 @@ export default () => ({
|
|
|
55
56
|
|
|
56
57
|
function get008Value() {
|
|
57
58
|
const value = record.get(/^008$/u)?.[0]?.value || undefined;
|
|
58
|
-
debugData(
|
|
59
|
+
debugData(`${label} 008 value: ${value}`);
|
|
59
60
|
|
|
60
61
|
if (!value) {
|
|
61
62
|
return undefined;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
const code = value.slice(35, 38);
|
|
65
|
-
debugData(
|
|
66
|
+
debugData(`${label} 008 code: ${code}`);
|
|
66
67
|
return code === '|||' || code === ' ' || code === '^^^' ? undefined : code;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
// Main language for the resource: in the first f041 $a or f041 $d
|
|
71
|
+
// Should we get all $a or $d languages instead of just the first?
|
|
70
72
|
// Uses only f041s that have 2nd ind ' ', which means that the codes used are MARC 21 language codes
|
|
71
73
|
|
|
72
74
|
function get041Values() {
|
|
@@ -43,7 +43,7 @@ export default () => {
|
|
|
43
43
|
|
|
44
44
|
return {extract, compare};
|
|
45
45
|
|
|
46
|
-
function extract(record) {
|
|
46
|
+
function extract({record, recordExternal}) {
|
|
47
47
|
|
|
48
48
|
const isMelindaRecord = record.get('003').some(f003 => f003.value === 'FI-MELINDA');
|
|
49
49
|
const [f001] = record.get('001').map(field => field.value);
|
|
@@ -54,7 +54,7 @@ export default () => {
|
|
|
54
54
|
f001 === undefined &&
|
|
55
55
|
f035MelindaIds.length < 1) {
|
|
56
56
|
|
|
57
|
-
debug(
|
|
57
|
+
debug(`${recordExternal.label} No Melinda-IDs found`);
|
|
58
58
|
return {};
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -62,7 +62,6 @@ export default () => {
|
|
|
62
62
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
// eslint-disable-next-line max-statements
|
|
66
65
|
function compare(a, b) {
|
|
67
66
|
|
|
68
67
|
if (a.isMelindaRecord && b.isMelindaRecord &&
|
|
@@ -33,7 +33,7 @@ import {testStringOrNumber} from '../../../matching-utils';
|
|
|
33
33
|
|
|
34
34
|
export default () => ({
|
|
35
35
|
name: 'Publication time',
|
|
36
|
-
extract: record => {
|
|
36
|
+
extract: ({record}) => {
|
|
37
37
|
const value = record.get(/^008$/u)?.[0]?.value || undefined;
|
|
38
38
|
return testStringOrNumber(value) ? [String(value).slice(7, 11)] : [];
|
|
39
39
|
},
|
|
@@ -26,27 +26,45 @@
|
|
|
26
26
|
*
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
import {LevenshteinDistance as leven} from 'natural';
|
|
31
31
|
import {testStringOrNumber} from '../../../matching-utils';
|
|
32
|
+
import createDebugLogger from 'debug';
|
|
33
|
+
|
|
34
|
+
const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:title');
|
|
35
|
+
const debugData = debug.extend('data');
|
|
36
|
+
|
|
32
37
|
|
|
33
38
|
export default ({treshold = 10} = {}) => ({
|
|
34
39
|
name: 'Title',
|
|
35
|
-
extract: record => {
|
|
40
|
+
extract: ({record, recordExternal}) => {
|
|
41
|
+
const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';
|
|
36
42
|
const title = getTitle();
|
|
43
|
+
debug(`${label} title: ${title}`);
|
|
37
44
|
|
|
38
45
|
if (testStringOrNumber(title)) {
|
|
39
|
-
|
|
46
|
+
const titleAsNormalizedString = String(title)
|
|
47
|
+
// decompose unicode diacritics
|
|
48
|
+
.normalize('NFD')
|
|
49
|
+
// strip non-letters/numbers
|
|
50
|
+
// - note: combined with decomposing unicode diactics this normalizes both 'saa' and 'sää' as 'saa'
|
|
51
|
+
// - we could precompose the finnish letters back to avoid this
|
|
52
|
+
.replace(/[^\p{Letter}\p{Number}]/gu, '')
|
|
53
|
+
.toLowerCase();
|
|
54
|
+
debug(`${label} titleString: ${titleAsNormalizedString}`);
|
|
55
|
+
return [titleAsNormalizedString];
|
|
40
56
|
}
|
|
41
57
|
|
|
42
58
|
return [];
|
|
43
59
|
|
|
44
60
|
function getTitle() {
|
|
45
61
|
const [field] = record.get(/^245$/u);
|
|
62
|
+
debugData(`${label} titleField: ${JSON.stringify(field)}`);
|
|
46
63
|
|
|
47
64
|
if (field) {
|
|
48
65
|
return field.subfields
|
|
49
|
-
|
|
66
|
+
// get also $n:s and $p:s here
|
|
67
|
+
.filter(({code}) => ['a', 'b', 'n', 'p'].includes(code))
|
|
50
68
|
.map(({value}) => testStringOrNumber(value) ? String(value) : '')
|
|
51
69
|
.join('');
|
|
52
70
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
1
|
/**
|
|
3
2
|
*
|
|
4
3
|
* @licstart The following is the entire license notice for the JavaScript code in this file.
|
|
@@ -32,36 +31,45 @@ import * as features from './features';
|
|
|
32
31
|
|
|
33
32
|
export {features};
|
|
34
33
|
|
|
35
|
-
export default ({strategy, treshold = 0.9}, returnStrategy = false) => (recordA, recordB) => {
|
|
34
|
+
export default ({strategy, treshold = 0.9}, returnStrategy = false, localSettings = {}) => ({recordA, recordB, recordAExternal = {}, recordBExternal = {}}) => {
|
|
36
35
|
const minProbabilityQuantifier = 0.5;
|
|
37
36
|
|
|
38
37
|
const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection');
|
|
39
38
|
const debugData = debug.extend('data');
|
|
40
39
|
|
|
41
|
-
debugData(`Strategy: ${JSON.stringify(strategy)}`);
|
|
42
|
-
debugData(`
|
|
43
|
-
|
|
40
|
+
debugData(`Strategy: ${JSON.stringify(strategy)}, Treshold: ${JSON.stringify(treshold)}, ReturnStrategy: ${JSON.stringify(returnStrategy)}`);
|
|
41
|
+
debugData(`Records: A: ${recordA}\nB: ${recordB}`);
|
|
42
|
+
debug(`Externals: A: ${JSON.stringify(recordAExternal)}, B: ${JSON.stringify(recordBExternal)}`);
|
|
43
|
+
// We could add here labels for records if we didn't get external labels
|
|
44
44
|
|
|
45
|
-
const featuresA = extractFeatures(recordA);
|
|
45
|
+
const featuresA = extractFeatures({record: recordA, recordExternal: recordAExternal, localSettings});
|
|
46
46
|
|
|
47
47
|
debug(`We got an array of records: ${Array.isArray(recordB)}`);
|
|
48
48
|
const recordsB = Array.isArray(recordB) ? recordB : [recordB];
|
|
49
|
-
const
|
|
49
|
+
const recordsBExternal = Array.isArray(recordB) ? recordBExternal : [recordBExternal];
|
|
50
50
|
|
|
51
|
+
const detectionResults = recordsB.map((record, index) => actualDetection({featuresA, recordAExternal, record, recordExternal: recordsBExternal[index], index}));
|
|
52
|
+
|
|
53
|
+
// if we got array of records, we return an array of result
|
|
54
|
+
// if we got a singular record, we return a singular result
|
|
51
55
|
return Array.isArray(recordB) ? detectionResults : detectionResults[0];
|
|
52
56
|
|
|
53
|
-
function actualDetection(featuresA, record) {
|
|
57
|
+
function actualDetection({featuresA, record, recordExternal, index, localSettings}) {
|
|
58
|
+
const labelA = recordAExternal && recordAExternal.label ? recordAExternal.label : 'a';
|
|
59
|
+
const labelB = recordExternal && recordExternal.label ? recordExternal.label : 'b';
|
|
60
|
+
|
|
54
61
|
|
|
55
|
-
|
|
62
|
+
debug(`Actual detection for record ${index + 1} ${labelB}`);
|
|
63
|
+
const featuresB = extractFeatures({record, recordExternal, localSettings});
|
|
56
64
|
|
|
57
|
-
debugData(`Features (a): ${JSON.stringify(featuresA)}`);
|
|
58
|
-
debugData(`Features (b): ${JSON.stringify(featuresB)}`);
|
|
65
|
+
debugData(`Features (a: ${labelA}): ${JSON.stringify(featuresA)}`);
|
|
66
|
+
debugData(`Features (b: ${labelB}): ${JSON.stringify(featuresB)}`);
|
|
59
67
|
|
|
60
68
|
const featurePairs = generateFeaturePairs(featuresA, featuresB);
|
|
61
69
|
const similarityVector = generateSimilarityVector(featurePairs);
|
|
62
70
|
|
|
63
71
|
if (similarityVector.some(v => v >= minProbabilityQuantifier)) {
|
|
64
|
-
const probability =
|
|
72
|
+
const probability = calculateProbability(similarityVector);
|
|
65
73
|
debug(`probability: ${probability} (Treshold: ${treshold})`);
|
|
66
74
|
return returnResult({match: probability >= treshold, probability});
|
|
67
75
|
}
|
|
@@ -70,8 +78,8 @@ export default ({strategy, treshold = 0.9}, returnStrategy = false) => (recordA,
|
|
|
70
78
|
return returnResult({match: false, probability: 0.0});
|
|
71
79
|
}
|
|
72
80
|
|
|
73
|
-
function extractFeatures(record) {
|
|
74
|
-
return strategy.reduce((acc, {name, extract}) => acc.concat({name, value: extract(record)}), []);
|
|
81
|
+
function extractFeatures({record, recordExternal}) {
|
|
82
|
+
return strategy.reduce((acc, {name, extract}) => acc.concat({name, value: extract({record, recordExternal})}), []);
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
function returnResult(result) {
|
|
@@ -89,7 +97,7 @@ export default ({strategy, treshold = 0.9}, returnStrategy = false) => (recordA,
|
|
|
89
97
|
return strategyNames || [];
|
|
90
98
|
}
|
|
91
99
|
|
|
92
|
-
function
|
|
100
|
+
function calculateProbability(similarityVector) {
|
|
93
101
|
const probability = similarityVector.reduce((acc, v) => acc + v, 0.0);
|
|
94
102
|
return probability > 1.0 ? 1.0 : probability;
|
|
95
103
|
}
|
|
@@ -63,7 +63,7 @@ describe('match-detection', () => {
|
|
|
63
63
|
: new MarcRecord(getFixture('recordB.json'), {subfieldValues: false});
|
|
64
64
|
debugData(inspect(recordB));
|
|
65
65
|
|
|
66
|
-
const results = detect(recordA, recordB);
|
|
66
|
+
const results = detect({recordA, recordB});
|
|
67
67
|
debugData(`${JSON.stringify(results)}`);
|
|
68
68
|
|
|
69
69
|
expect(results).to.eql(expectedResults);
|
package/src/matching-utils.js
CHANGED
|
@@ -31,7 +31,6 @@ import createDebugLogger from 'debug';
|
|
|
31
31
|
const debug = createDebugLogger('@natlibfi/melinda-record-matching:utils');
|
|
32
32
|
const debugData = debug.extend('data');
|
|
33
33
|
|
|
34
|
-
// eslint-disable-next-line max-statements
|
|
35
34
|
export function getMelindaIdsF035(record) {
|
|
36
35
|
|
|
37
36
|
const debug = createDebugLogger('@natlibfi/melinda-record-matching:melinda-id');
|