@natlibfi/melinda-record-matching 1.0.6 → 2.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/candidate-search/candidate-search-utils.js +58 -0
  2. package/dist/candidate-search/candidate-search-utils.js.map +1 -0
  3. package/dist/candidate-search/index.js +4 -4
  4. package/dist/candidate-search/index.js.map +1 -1
  5. package/dist/candidate-search/index.spec.js +94 -86
  6. package/dist/candidate-search/index.spec.js.map +1 -1
  7. package/dist/candidate-search/query-list/bib.js +103 -20
  8. package/dist/candidate-search/query-list/bib.js.map +1 -1
  9. package/dist/candidate-search/query-list/bib.spec.js +25 -23
  10. package/dist/candidate-search/query-list/bib.spec.js.map +1 -1
  11. package/dist/candidate-search/query-list/index.js +6 -4
  12. package/dist/candidate-search/query-list/index.js.map +1 -1
  13. package/dist/index.js +3 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.spec.js +53 -46
  16. package/dist/index.spec.js.map +1 -1
  17. package/dist/match-detection/features/bib/all-source-ids.js +122 -0
  18. package/dist/match-detection/features/bib/all-source-ids.js.map +1 -0
  19. package/dist/match-detection/features/bib/authors.js.map +1 -1
  20. package/dist/match-detection/features/bib/bibliographic-level.js.map +1 -1
  21. package/dist/match-detection/features/bib/host-component.js.map +1 -1
  22. package/dist/match-detection/features/bib/index.js +32 -16
  23. package/dist/match-detection/features/bib/index.js.map +1 -1
  24. package/dist/match-detection/features/bib/index.spec.js +50 -42
  25. package/dist/match-detection/features/bib/index.spec.js.map +1 -1
  26. package/dist/match-detection/features/bib/isbn.js.map +1 -1
  27. package/dist/match-detection/features/bib/issn.js.map +1 -1
  28. package/dist/match-detection/features/bib/language.js +42 -5
  29. package/dist/match-detection/features/bib/language.js.map +1 -1
  30. package/dist/match-detection/features/bib/melinda-id.js +52 -0
  31. package/dist/match-detection/features/bib/melinda-id.js.map +1 -0
  32. package/dist/match-detection/features/bib/melinda-identifier-factory.js +100 -0
  33. package/dist/match-detection/features/bib/melinda-identifier-factory.js.map +1 -0
  34. package/dist/match-detection/features/bib/other-standard-identifier.js.map +1 -1
  35. package/dist/match-detection/features/bib/publication-time.js.map +1 -1
  36. package/dist/match-detection/features/bib/record-type.js.map +1 -1
  37. package/dist/match-detection/features/bib/standard-identifier-factory.js +8 -0
  38. package/dist/match-detection/features/bib/standard-identifier-factory.js.map +1 -1
  39. package/dist/match-detection/features/bib/title.js.map +1 -1
  40. package/dist/match-detection/features/index.js +2 -2
  41. package/dist/match-detection/index.js +10 -6
  42. package/dist/match-detection/index.js.map +1 -1
  43. package/dist/match-detection/index.spec.js +32 -25
  44. package/dist/match-detection/index.spec.js.map +1 -1
  45. package/dist/matching-utils.js +95 -0
  46. package/dist/matching-utils.js.map +1 -0
  47. package/package.json +26 -29
  48. package/src/candidate-search/candidate-search-utils.js +53 -0
  49. package/src/candidate-search/index.js +1 -1
  50. package/src/candidate-search/index.spec.js +64 -58
  51. package/src/candidate-search/query-list/bib.js +120 -19
  52. package/src/candidate-search/query-list/bib.spec.js +16 -14
  53. package/src/candidate-search/query-list/index.js +3 -1
  54. package/src/index.spec.js +46 -39
  55. package/src/match-detection/features/bib/all-source-ids.js +122 -0
  56. package/src/match-detection/features/bib/index.js +2 -0
  57. package/src/match-detection/features/bib/index.spec.js +32 -23
  58. package/src/match-detection/features/bib/language.js +37 -3
  59. package/src/match-detection/features/bib/melinda-id.js +34 -0
  60. package/src/match-detection/features/bib/melinda-identifier-factory.js +94 -0
  61. package/src/match-detection/features/bib/standard-identifier-factory.js +6 -0
  62. package/src/match-detection/index.js +7 -4
  63. package/src/match-detection/index.spec.js +27 -20
  64. package/src/matching-utils.js +86 -0
@@ -32,78 +32,84 @@ import generateTests from '@natlibfi/fixugen-http-client';
32
32
  import {MarcRecord} from '@natlibfi/marc-record';
33
33
  import createSearchInterface, {CandidateSearchError} from '.';
34
34
 
35
- generateTests({
36
- callback,
37
- path: [__dirname, '..', '..', 'test-fixtures', 'candidate-search', 'index'],
38
- recurse: false,
39
- fixura: {
40
- reader: READERS.JSON
41
- }
42
- });
35
+ describe('candidate-search', () => {
36
+ generateTests({
37
+ callback,
38
+ path: [__dirname, '..', '..', 'test-fixtures', 'candidate-search', 'index'],
39
+ recurse: false,
40
+ fixura: {
41
+ reader: READERS.JSON
42
+ }
43
+ });
43
44
 
44
- // eslint-disable-next-line max-statements
45
- async function callback({getFixture, factoryOptions, searchOptions, expectedFactoryError, expectedSearchError}) {
46
- const url = 'http://foo.bar';
45
+ // eslint-disable-next-line max-statements
46
+ async function callback({getFixture, factoryOptions, searchOptions, expectedFactoryError, expectedSearchError, enabled = true}) {
47
+ const url = 'http://foo.bar';
47
48
 
48
- if (expectedFactoryError) {
49
- if (expectedFactoryError.isCandidateSearchError) {
50
- expect(() => createSearchInterface({...formatFactoryOptions(), url})).to.throw(CandidateSearchError, new RegExp(expectedFactoryError, 'u'));
49
+ if (!enabled) {
51
50
  return;
52
51
  }
53
52
 
54
- expect(() => createSearchInterface({...formatFactoryOptions(), url})).to.throw(new RegExp(expectedFactoryError, 'u'));
55
- return;
56
- }
53
+ if (expectedFactoryError) {
54
+ if (expectedFactoryError.isCandidateSearchError) {
55
+ expect(() => createSearchInterface({...formatFactoryOptions(), url})).to.throw(CandidateSearchError, new RegExp(expectedFactoryError, 'u'));
56
+ return;
57
+ }
57
58
 
58
- const search = createSearchInterface({...formatFactoryOptions(), url});
59
- await iterate({searchOptions});
59
+ expect(() => createSearchInterface({...formatFactoryOptions(), url})).to.throw(new RegExp(expectedFactoryError, 'u'));
60
+ return;
61
+ }
60
62
 
61
- function formatFactoryOptions() {
62
- return {
63
- ...factoryOptions,
64
- maxRecordsPerRequest: 1,
65
- record: new MarcRecord(factoryOptions.record, {subfieldValues: false})
66
- };
67
- }
63
+ const search = createSearchInterface({...formatFactoryOptions(), url});
64
+ await iterate({searchOptions});
68
65
 
69
- // eslint-disable-next-line max-statements
70
- async function iterate({searchOptions, count = 1}) {
71
- const expectedResults = getFixture(`expectedResults${count}.json`);
66
+ function formatFactoryOptions() {
67
+ return {
68
+ ...factoryOptions,
69
+ maxRecordsPerRequest: 1,
70
+ record: new MarcRecord(factoryOptions.record, {subfieldValues: false})
71
+ };
72
+ }
72
73
 
73
- if (expectedSearchError) { // eslint-disable-line functional/no-conditional-statement
74
- try {
75
- await search(searchOptions);
76
- throw new Error('Expected an error');
77
- } catch (err) {
78
- expect(err).to.be.an('error');
79
- expect(err.message).to.match(new RegExp(expectedSearchError, 'u'));
80
- return;
74
+ // eslint-disable-next-line max-statements
75
+ async function iterate({searchOptions, count = 1}) {
76
+ const expectedResults = getFixture(`expectedResults${count}.json`);
77
+
78
+ if (expectedSearchError) { // eslint-disable-line functional/no-conditional-statement
79
+ try {
80
+ await search(searchOptions);
81
+ throw new Error('Expected an error');
82
+ } catch (err) {
83
+ expect(err).to.be.an('error');
84
+ expect(err.message).to.match(new RegExp(expectedSearchError, 'u'));
85
+ return;
86
+ }
81
87
  }
82
- }
83
88
 
84
- const results = await search(searchOptions);
89
+ const results = await search(searchOptions);
85
90
 
86
- expect(formatResults(results)).to.eql(expectedResults);
91
+ expect(formatResults(results)).to.eql(expectedResults);
87
92
 
88
- if (results.records.length > 0) {
89
- return iterate({
90
- searchOptions: resultsToOptions(results),
91
- count: count + 1
92
- });
93
- }
93
+ if (results.records.length > 0) {
94
+ return iterate({
95
+ searchOptions: resultsToOptions(results),
96
+ count: count + 1
97
+ });
98
+ }
94
99
 
95
- function formatResults(results) {
96
- // console.log(results); //eslint-disable-line
97
- return {
98
- ...results,
99
- records: results.records.map(({record, id}) => ({id, record: record.toObject()}))
100
- };
101
- }
100
+ function formatResults(results) {
101
+ // console.log(results); //eslint-disable-line
102
+ return {
103
+ ...results,
104
+ records: results.records.map(({record, id}) => ({id, record: record.toObject()}))
105
+ };
106
+ }
102
107
 
103
- function resultsToOptions(results) {
104
- return Object.entries(results)
105
- .filter(([k]) => k === 'records' === false)
106
- .reduce((acc, [k, v]) => ({...acc, [k]: v}), {});
108
+ function resultsToOptions(results) {
109
+ return Object.entries(results)
110
+ .filter(([k]) => k === 'records' === false) // If key is 'records' return false
111
+ .reduce((acc, [k, v]) => ({...acc, [k]: v}), {});
112
+ }
107
113
  }
108
114
  }
109
- }
115
+ });
@@ -27,6 +27,125 @@
27
27
  *
28
28
  */
29
29
  import createDebugLogger from 'debug';
30
+ import {toQueries} from '../candidate-search-utils';
31
+ import {getMelindaIdsF035, validateSidFieldSubfieldCounts, getSubfieldValues} from '../../matching-utils';
32
+
33
+
34
+ export function bibSourceIds(record) {
35
+
36
+ /* Melinda's SRU-index melinda.sourceid includes source IDs from SID fields in Melinda records
37
+ SID-fields in Melinda have sf $c with local id and sf $b with a code for the local db:
38
+
39
+ SID__ $c 123457 $b helka
40
+ SID__ $c (ANDL100020)1077305 $b sata
41
+ SID__ $c VER999999 $ FI-KV
42
+ SID__ $c /10024/508126 $ REPO_THESEUS
43
+
44
+ In melinda.sourceid -index case is kept, sourceprefixes in brackets and hyphens are normalized away:
45
+ Note: slashes are not normalized away, but a SRU-search-string including slashes needs to be quoted
46
+
47
+ 1234567helka
48
+ 1077305sata
49
+ VER999999FIKV
50
+ /10024/508126REPO_THESEUS
51
+
52
+ Note: All Melinda records that have a matching records in a local db do NOT have SID for that local records,
53
+ existence of a SID field depends on how the record has been added to Melinda and how it has been handled
54
+ afterwards. SIDs are also not reliably maintained. A record might or might not have a SID for a local db
55
+ after the matching record is removed from the local db.
56
+
57
+ */
58
+
59
+
60
+ const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:source-ids');
61
+ const debugData = debug.extend('data');
62
+ //const debugInfo = debug.extend('info');
63
+
64
+ debug(`Creating queries for sourceid's`);
65
+
66
+ const fSids = record.get('SID');
67
+ debugData(`SID-fields (${fSids.length}): ${JSON.stringify(fSids)}`);
68
+
69
+ return fSids.length > 0 ? toSidQueries(fSids) : [];
70
+
71
+ function toSidQueries(fSids) {
72
+ debug(`Creating actual queries for sourceid's`);
73
+
74
+ const sidStrings = getSidStrings(fSids);
75
+
76
+ if (sidStrings.length < 1) {
77
+ debug(`No identifiers found, no queries created.`);
78
+ return [];
79
+ }
80
+
81
+ const sidQueries = toQueries(sidStrings, 'melinda.sourceid');
82
+
83
+ return sidQueries;
84
+
85
+ function getSidStrings(fSids) {
86
+ debug(`Getting Sid strings from SID fields`);
87
+
88
+ // Map SID fields to valid sidStrings, filter out empty strings
89
+ const sidStrings = fSids.map(toSidString).filter(nonEmptySid => nonEmptySid);
90
+ return sidStrings;
91
+
92
+ function toSidString(field) {
93
+ debug(`Getting string from a field`);
94
+
95
+ return validateSidFieldSubfieldCounts(field) ? createSidString(field) : '';
96
+
97
+ function createSidString(field) {
98
+ debug(`Creating string from a field`);
99
+ const [sfC] = getSubfieldValues(field, 'c');
100
+ const [sfB] = getSubfieldValues(field, 'b');
101
+
102
+ const cleanedSfC = removeSourcePrefix(normalizeSidSubfieldValue(sfC));
103
+ const cleanedSfB = normalizeSidSubfieldValue(sfB);
104
+
105
+ debugData(`${JSON.stringify(sfC)} + ${JSON.stringify(sfB)}`);
106
+ return cleanedSfC.concat(cleanedSfB);
107
+ }
108
+
109
+ function removeSourcePrefix(subfieldValue) {
110
+ const sourcePrefixRegex = (/^(?<sourcePrefix>\([A-Za-z0-9-]+\))(?<id>.+)$/u);
111
+ const normalizedValue = subfieldValue.replace(sourcePrefixRegex, '$<id>');
112
+ debugData(`Normalized ${subfieldValue} to ${normalizedValue}`);
113
+ return normalizedValue;
114
+ }
115
+
116
+ function normalizeSidSubfieldValue(subfieldValue) {
117
+ debugData(`Normalizing ${subfieldValue}`);
118
+ const normalizeAwayRegex = (/[- ]/u);
119
+ return subfieldValue.replace(normalizeAwayRegex, '');
120
+ }
121
+
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ export function bibMelindaIds(record) {
128
+ // Melinda's SRU-index melinda.melindaid includes f001 controlnumbers and old Melinda-IDs from f035z's for all non-deleted Melinda-records
129
+
130
+ const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibMelindaIds');
131
+ const debugData = debug.extend('data');
132
+ debug(`Creating queries for MelindaIds`);
133
+
134
+ // Note: Melinda-ID's for search queries are created just from records f035a's and f035z's
135
+ // Both (FI-MELINDA)- and FCC-prefixed forms are found
136
+ // f001 controlnumber is not currently included, even if record's f003 is FI-MELINDA
137
+ const melindaIds = getMelindaIdsF035(record);
138
+
139
+ debugData(`Unique identifiers (${melindaIds.length}): ${JSON.stringify(melindaIds)}`);
140
+
141
+ if (melindaIds.length < 1) {
142
+ debug(`No identifiers found, no queries created.`);
143
+ return [];
144
+ }
145
+
146
+ return toQueries(melindaIds, 'melinda.melindaid');
147
+ }
148
+
30
149
 
31
150
  // bibHostComponents returns host id from the first subfield $w of first field f773, see test-fixtures 04 and 05
32
151
  // bibHostComponents should search all 773 $ws for possible host id, but what should it do in case of multiple host ids?
@@ -103,25 +222,7 @@ export function bibStandardIdentifiers(record) {
103
222
  return [];
104
223
  }
105
224
 
106
- return toQueries(uniqueIdentifiers);
107
-
108
- function toQueries(identifiers) {
109
- // Aleph supports only two queries with or -operator (This is not actually true)
110
- const pairs = toPairs(identifiers);
111
- const queries = pairs.map(([a, b]) => b ? `dc.identifier=${a} or dc.identifier=${b}` : `dc.identifier=${a}`);
112
-
113
- debugData(`Pairs (${pairs.length}): ${JSON.stringify(pairs)}`);
114
- debugData(`Queries (${queries.length}): ${JSON.stringify(queries)}`);
115
-
116
- return queries;
117
-
118
- function toPairs(array) {
119
- if (array.length === 0) {
120
- return [];
121
- }
122
- return [array.slice(0, 2)].concat(toPairs(array.slice(2), 2));
123
- }
124
- }
225
+ return toQueries(uniqueIdentifiers, 'dc.identifier');
125
226
 
126
227
  function toIdentifiers({tag, subfields}) {
127
228
  const issnIsbnReqExp = (/^[A-Za-z0-9-]+$/u);
@@ -32,20 +32,22 @@ import {expect} from 'chai';
32
32
  import {MarcRecord} from '@natlibfi/marc-record';
33
33
  import * as generators from './bib';
34
34
 
35
- generateTests({
36
- path: [__dirname, '..', '..', '..', 'test-fixtures', 'candidate-search', 'query-list', 'bib'],
37
- useMetadataFile: true,
38
- fixura: {
39
- reader: READERS.JSON
40
- },
41
- callback: ({type, inputRecord, expectedQuery, enabled = true}) => {
42
- const generate = generators[type];
43
- const record = new MarcRecord(inputRecord, {subfieldValues: false});
35
+ describe('candidate-search/query-list/bib/', () => {
36
+ generateTests({
37
+ path: [__dirname, '..', '..', '..', 'test-fixtures', 'candidate-search', 'query-list', 'bib'],
38
+ useMetadataFile: true,
39
+ fixura: {
40
+ reader: READERS.JSON
41
+ },
42
+ callback: ({type, inputRecord, expectedQuery, enabled = true}) => {
43
+ const generate = generators[type];
44
+ const record = new MarcRecord(inputRecord, {subfieldValues: false});
44
45
 
45
- if (!enabled) {
46
- return;
47
- }
46
+ if (!enabled) {
47
+ return;
48
+ }
48
49
 
49
- expect(generate(record)).to.eql(expectedQuery);
50
- }
50
+ expect(generate(record)).to.eql(expectedQuery);
51
+ }
52
+ });
51
53
  });
@@ -32,7 +32,9 @@ export const searchTypes = {
32
32
  bib: {
33
33
  standardIdentifiers: 'bibStandardIdentifiers',
34
34
  hostComponents: 'bibHostComponents',
35
- title: 'bibTitle'
35
+ title: 'bibTitle',
36
+ melindaId: 'bibMelindaIds',
37
+ sourceIds: 'bibSourceIds'
36
38
  }
37
39
  };
38
40
 
package/src/index.spec.js CHANGED
@@ -32,49 +32,56 @@ import generateTests from '@natlibfi/fixugen-http-client';
32
32
  import {MarcRecord} from '@natlibfi/marc-record';
33
33
  import createMatchInterface, {matchDetection} from '.';
34
34
 
35
- generateTests({
36
- callback,
37
- path: [__dirname, '..', 'test-fixtures', 'index'],
38
- recurse: false,
39
- fixura: {
40
- reader: READERS.JSON
41
- }
42
- });
35
+ describe('INDEX', () => {
36
+ generateTests({
37
+ callback,
38
+ path: [__dirname, '..', 'test-fixtures', 'index'],
39
+ recurse: false,
40
+ fixura: {
41
+ reader: READERS.JSON
42
+ }
43
+ });
43
44
 
44
- async function callback({getFixture, options}) {
45
- const record = new MarcRecord(getFixture('inputRecord.json'), {subfieldValues: false});
46
- const expectedMatches = getFixture('expectedMatches.json');
45
+ async function callback({getFixture, options, enabled = true}) {
47
46
 
48
- const match = createMatchInterface(formatOptions());
49
- const matches = await match(record);
47
+ if (!enabled) {
48
+ return;
49
+ }
50
50
 
51
- expect(formatResults()).to.eql(expectedMatches);
51
+ const record = new MarcRecord(getFixture('inputRecord.json'), {subfieldValues: false});
52
+ const expectedMatches = getFixture('expectedMatches.json');
52
53
 
53
- function formatOptions() {
54
- const contextFeatures = matchDetection.features[options.detection.strategy.type];
54
+ const match = createMatchInterface(formatOptions());
55
+ const matches = await match(record);
55
56
 
56
- return {
57
- ...options,
58
- search: {
59
- ...options.search,
60
- maxRecordsPerRequest: 1
61
- },
62
- detection: {
63
- ...options.detect,
64
- strategy: options.detection.strategy.features.map(v => contextFeatures[v]())
65
- },
66
- maxMatches: 2,
67
- maxCandidates: 1
68
- };
69
- }
57
+ expect(formatResults()).to.eql(expectedMatches);
70
58
 
71
- function formatResults() {
72
- return matches.map(({candidate, probability}) => ({
73
- probability,
74
- candidate: {
75
- id: candidate.id,
76
- record: candidate.record.toObject()
77
- }
78
- }));
59
+ function formatOptions() {
60
+ const contextFeatures = matchDetection.features[options.detection.strategy.type];
61
+
62
+ return {
63
+ ...options,
64
+ search: {
65
+ ...options.search,
66
+ maxRecordsPerRequest: 1
67
+ },
68
+ detection: {
69
+ ...options.detect,
70
+ strategy: options.detection.strategy.features.map(v => contextFeatures[v]())
71
+ },
72
+ maxMatches: 2,
73
+ maxCandidates: 1
74
+ };
75
+ }
76
+
77
+ function formatResults() {
78
+ return matches.map(({candidate, probability}) => ({
79
+ probability,
80
+ candidate: {
81
+ id: candidate.id,
82
+ record: candidate.record.toObject()
83
+ }
84
+ }));
85
+ }
79
86
  }
80
- }
87
+ });
@@ -0,0 +1,122 @@
1
+ /* eslint-disable max-statements */
2
+ /**
3
+ *
4
+ * @licstart The following is the entire license notice for the JavaScript code in this file.
5
+ *
6
+ * Melinda record matching modules for Javascript
7
+ *
8
+ * Copyright (C) 2020 University Of Helsinki (The National Library Of Finland)
9
+ *
10
+ * This file is part of melinda-record-matching-js
11
+ *
12
+ * melinda-record-matching-js program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Lesser General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * melinda-record-matching-js is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Lesser General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ *
25
+ * @licend The above is the entire license notice
26
+ * for the JavaScript code in this file.
27
+ *
28
+ */
29
+
30
+ import createDebugLogger from 'debug';
31
+ import {validateSidFieldSubfieldCounts, getSubfieldValues} from '../../../matching-utils';
32
+
33
+ const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:all-source-ids');
34
+ const debugData = debug.extend('data');
35
+
36
+ /*
37
+
38
+ SID-fields in Melinda have sf $c with local id and sf $b with a code for the local db:
39
+
40
+ SID__ $c 123457 $b helka
41
+ SID__ $c (ANDL100020)1077305 $b sata
42
+ SID__ $c VER999999 $ FI-KV
43
+
44
+ allSourceIds matching feature is formatted as [{sourceDb: "helka", sourceId: "123457"}, {sourceDb: "sata", sourceId: "(ANDL100020)1077305"}, {sourceDb: "FI-KV", sourceId: "VER999999"}]
45
+ - no normalization / prefix deletions etc. in the feature
46
+
47
+ Note: All Melinda records that have a matching records in a local db do NOT have SID for that local records,
48
+ existence of a SID field depends on how the record has been added to Melinda and how it has been handled
49
+ afterwards. SIDs are also not reliably maintained. A record might or might not have a SID for a local db
50
+ after the matching record is removed from the local db.
51
+
52
+ Records with mismatching local ids for matching local db are a strong mismatch.
53
+ Records with matching local ids for matching local db are a very good match.
54
+
55
+ */
56
+
57
+ export default () => ({
58
+ name: 'All source IDs',
59
+ extract: record => {
60
+ debug(`Creating match detection features for all local source id's`);
61
+
62
+ const fSids = record.get('SID');
63
+ debugData(`SID-fields (${fSids.length}): ${JSON.stringify(fSids)}`);
64
+
65
+ const sidFeatures = getSidFeatures(fSids);
66
+ debugData(`SID-features (${sidFeatures.length}): ${JSON.stringify(sidFeatures)}`);
67
+
68
+ return sidFeatures;
69
+
70
+ function getSidFeatures(fSids) {
71
+ debug(`Getting Sid strings from SID fields`);
72
+
73
+ // Map SID fields to sidFeatures, filter out empty strings
74
+ const sidFeatures = fSids.map(toSidFeature).filter(nonEmptySid => nonEmptySid);
75
+ return sidFeatures;
76
+
77
+ function toSidFeature(field) {
78
+ debug(`Getting feature from a field`);
79
+
80
+ return validateSidFieldSubfieldCounts(field) ? createSidFeature(field) : '';
81
+
82
+ function createSidFeature(field) {
83
+ debug(`Creating feature from a field`);
84
+ const [sfC] = getSubfieldValues(field, 'c');
85
+ const [sfB] = getSubfieldValues(field, 'b');
86
+
87
+ debugData(`${JSON.stringify(sfC)} + ${JSON.stringify(sfB)}`);
88
+ debugData(`sourceDb: ${sfB}, sourceId: ${sfC}`);
89
+ return {'sourceDb': sfB, 'sourceId': sfC};
90
+ }
91
+ }
92
+
93
+ }
94
+
95
+ },
96
+ compare: (a, b) => {
97
+
98
+ debugData(`Comparing ${JSON.stringify(a)} and ${JSON.stringify(b)}`);
99
+
100
+ const matches = a.filter(sidA => b.some(sidB => sidA.sourceDb === sidB.sourceDb &&
101
+ sidA.sourceId === sidB.sourceId));
102
+ debugData(`Matches (${matches.length}): ${JSON.stringify(matches)}`);
103
+
104
+ const mismatches = a.filter(sidA => b.some(sidB => sidA.sourceDb === sidB.sourceDb &&
105
+ sidA.sourceId !== sidB.sourceId));
106
+ debugData(`Mismatches (${mismatches.length}): ${JSON.stringify(mismatches)}`);
107
+
108
+ // If there's at least one mismatching source ID from matching source db
109
+ if (mismatches.length > 0) {
110
+ return -1;
111
+ }
112
+
113
+ // If there's at least one matching source ID from matching source db
114
+ if (matches.length > 0) {
115
+ return 1;
116
+ }
117
+
118
+ // If there are no source IDs with matching sources
119
+ return 0;
120
+
121
+ }
122
+ });
@@ -36,3 +36,5 @@ export {default as recordType} from './record-type';
36
36
  export {default as publicationTime} from './publication-time';
37
37
  export {default as language} from './language';
38
38
  export {default as bibliographicLevel} from './bibliographic-level';
39
+ export {default as melindaId} from './melinda-id';
40
+ export {default as allSourceIds} from './all-source-ids';
@@ -32,30 +32,39 @@ import {expect} from 'chai';
32
32
  import {MarcRecord} from '@natlibfi/marc-record';
33
33
  import * as features from '.';
34
34
 
35
- generateTests({
36
- path: [__dirname, '..', '..', '..', '..', 'test-fixtures', 'match-detection', 'features', 'bib'],
37
- useMetadataFile: true,
38
- fixura: {
39
- reader: READERS.JSON
40
- },
41
- callback: ({feature, options, type, ...expectations}) => {
42
- if (type === 'extract') {
43
- const {expectedFeatures, inputRecord} = expectations;
44
- const record = new MarcRecord(inputRecord, {subfieldValues: false});
45
- const {extract} = features[feature](options);
46
-
47
- expect(extract(record)).to.eql(expectedFeatures);
48
- return;
49
- }
50
35
 
51
- if (type === 'compare') {
52
- const {featuresA, featuresB, expectedPoints} = expectations;
53
- const {compare} = features[feature](options);
36
+ describe('match-detection/features/bib/', () => {
37
+ generateTests({
38
+ path: [__dirname, '..', '..', '..', '..', 'test-fixtures', 'match-detection', 'features', 'bib'],
39
+ useMetadataFile: true,
40
+ fixura: {
41
+ reader: READERS.JSON
42
+ },
43
+ // eslint-disable-next-line max-statements
44
+ callback: ({enabled = true, feature, options, type, ...expectations}) => {
54
45
 
55
- expect(compare(featuresA, featuresB)).to.equal(expectedPoints);
56
- return;
57
- }
46
+ if (!enabled) {
47
+ return;
48
+ }
49
+
50
+ if (type === 'extract') {
51
+ const {expectedFeatures, inputRecord} = expectations;
52
+ const record = new MarcRecord(inputRecord, {subfieldValues: false});
53
+ const {extract} = features[feature](options);
54
+
55
+ expect(extract(record)).to.eql(expectedFeatures);
56
+ return;
57
+ }
58
58
 
59
- throw new Error(`Invalid type ${type}`);
60
- }
59
+ if (type === 'compare') {
60
+ const {featuresA, featuresB, expectedPoints} = expectations;
61
+ const {compare} = features[feature](options);
62
+
63
+ expect(compare(featuresA, featuresB)).to.equal(expectedPoints);
64
+ return;
65
+ }
66
+
67
+ throw new Error(`Invalid type ${type}`);
68
+ }
69
+ });
61
70
  });