@natlibfi/melinda-record-matching 5.0.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 (71) hide show
  1. package/dist/candidate-search/candidate-search-utils.js +18 -0
  2. package/dist/candidate-search/candidate-search-utils.js.map +7 -0
  3. package/dist/candidate-search/choose-queries.js +75 -0
  4. package/dist/candidate-search/choose-queries.js.map +7 -0
  5. package/dist/candidate-search/index.js +118 -0
  6. package/dist/candidate-search/index.js.map +7 -0
  7. package/dist/candidate-search/index.test.js +92 -0
  8. package/dist/candidate-search/index.test.js.map +7 -0
  9. package/dist/candidate-search/query-list/bib.js +260 -0
  10. package/dist/candidate-search/query-list/bib.js.map +7 -0
  11. package/dist/candidate-search/query-list/bib.test.js +34 -0
  12. package/dist/candidate-search/query-list/bib.test.js.map +7 -0
  13. package/dist/candidate-search/query-list/component.js +87 -0
  14. package/dist/candidate-search/query-list/component.js.map +7 -0
  15. package/dist/candidate-search/query-list/index.js +53 -0
  16. package/dist/candidate-search/query-list/index.js.map +7 -0
  17. package/dist/cli.js +138 -0
  18. package/dist/cli.js.map +7 -0
  19. package/dist/index.js +204 -0
  20. package/dist/index.js.map +7 -0
  21. package/dist/index.test.js +69 -0
  22. package/dist/index.test.js.map +7 -0
  23. package/dist/match-detection/features/bib/all-source-ids.js +48 -0
  24. package/dist/match-detection/features/bib/all-source-ids.js.map +7 -0
  25. package/dist/match-detection/features/bib/authors.js +57 -0
  26. package/dist/match-detection/features/bib/authors.js.map +7 -0
  27. package/dist/match-detection/features/bib/bibliographic-level.js +6 -0
  28. package/dist/match-detection/features/bib/bibliographic-level.js.map +7 -0
  29. package/dist/match-detection/features/bib/host-component.js +6 -0
  30. package/dist/match-detection/features/bib/host-component.js.map +7 -0
  31. package/dist/match-detection/features/bib/index.js +17 -0
  32. package/dist/match-detection/features/bib/index.js.map +7 -0
  33. package/dist/match-detection/features/bib/index.test.js +40 -0
  34. package/dist/match-detection/features/bib/index.test.js.map +7 -0
  35. package/dist/match-detection/features/bib/isbn.js +21 -0
  36. package/dist/match-detection/features/bib/isbn.js.map +7 -0
  37. package/dist/match-detection/features/bib/issn.js +6 -0
  38. package/dist/match-detection/features/bib/issn.js.map +7 -0
  39. package/dist/match-detection/features/bib/language.js +80 -0
  40. package/dist/match-detection/features/bib/language.js.map +7 -0
  41. package/dist/match-detection/features/bib/media-type.js +31 -0
  42. package/dist/match-detection/features/bib/media-type.js.map +7 -0
  43. package/dist/match-detection/features/bib/melinda-id.js +6 -0
  44. package/dist/match-detection/features/bib/melinda-id.js.map +7 -0
  45. package/dist/match-detection/features/bib/melinda-identifier-factory.js +38 -0
  46. package/dist/match-detection/features/bib/melinda-identifier-factory.js.map +7 -0
  47. package/dist/match-detection/features/bib/other-standard-identifier.js +6 -0
  48. package/dist/match-detection/features/bib/other-standard-identifier.js.map +7 -0
  49. package/dist/match-detection/features/bib/publication-time-allow-cons-years-multi.js +116 -0
  50. package/dist/match-detection/features/bib/publication-time-allow-cons-years-multi.js.map +7 -0
  51. package/dist/match-detection/features/bib/publication-time-allow-cons-years.js +28 -0
  52. package/dist/match-detection/features/bib/publication-time-allow-cons-years.js.map +7 -0
  53. package/dist/match-detection/features/bib/publication-time.js +10 -0
  54. package/dist/match-detection/features/bib/publication-time.js.map +7 -0
  55. package/dist/match-detection/features/bib/record-type.js +6 -0
  56. package/dist/match-detection/features/bib/record-type.js.map +7 -0
  57. package/dist/match-detection/features/bib/standard-identifier-factory.js +99 -0
  58. package/dist/match-detection/features/bib/standard-identifier-factory.js.map +7 -0
  59. package/dist/match-detection/features/bib/title-version-original.js +37 -0
  60. package/dist/match-detection/features/bib/title-version-original.js.map +7 -0
  61. package/dist/match-detection/features/bib/title.js +46 -0
  62. package/dist/match-detection/features/bib/title.js.map +7 -0
  63. package/dist/match-detection/features/index.js +3 -0
  64. package/dist/match-detection/features/index.js.map +7 -0
  65. package/dist/match-detection/index.js +86 -0
  66. package/dist/match-detection/index.js.map +7 -0
  67. package/dist/match-detection/index.test.js +44 -0
  68. package/dist/match-detection/index.test.js.map +7 -0
  69. package/dist/matching-utils.js +79 -0
  70. package/dist/matching-utils.js.map +7 -0
  71. package/package.json +2 -1
@@ -0,0 +1,18 @@
1
+ import createDebugLogger from "debug";
2
+ export function toQueries(identifiers, queryString) {
3
+ const debug = createDebugLogger("@natlibfi/melinda-record-matching:toQueries");
4
+ const debugData = debug.extend("data");
5
+ const quotedIdentifiers = identifiers.map((identifier) => identifier.match(/\//u) ? `"${identifier}"` : `${identifier}`);
6
+ const pairs = toPairs(quotedIdentifiers);
7
+ const queries = pairs.map(([a, b]) => b ? `${queryString}=${a} or ${queryString}=${b}` : `${queryString}=${a}`);
8
+ debugData(`Pairs (${pairs.length}): ${JSON.stringify(pairs)}`);
9
+ debugData(`Queries (${queries.length}): ${JSON.stringify(queries)}`);
10
+ return queries;
11
+ }
12
+ function toPairs(array) {
13
+ if (array.length === 0) {
14
+ return [];
15
+ }
16
+ return [array.slice(0, 2)].concat(toPairs(array.slice(2), 2));
17
+ }
18
+ //# sourceMappingURL=candidate-search-utils.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/candidate-search/candidate-search-utils.js"],
4
+ "sourcesContent": ["import createDebugLogger from 'debug';\n\nexport function toQueries(identifiers, queryString) {\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:toQueries');\n const debugData = debug.extend('data');\n\n // We quote the identifier, if it contains a slash! (Slash in non-quoted SRU-search breaks search.)\n const quotedIdentifiers = identifiers.map(identifier => identifier.match(/\\//u) ? `\"${identifier}\"` : `${identifier}`);\n\n // Aleph supports only two queries with or -operator (This is not actually true)\n const pairs = toPairs(quotedIdentifiers);\n const queries = pairs.map(([a, b]) => b ? `${queryString}=${a} or ${queryString}=${b}` : `${queryString}=${a}`);\n\n debugData(`Pairs (${pairs.length}): ${JSON.stringify(pairs)}`);\n debugData(`Queries (${queries.length}): ${JSON.stringify(queries)}`);\n\n return queries;\n}\n\nfunction toPairs(array) {\n if (array.length === 0) {\n return [];\n }\n return [array.slice(0, 2)].concat(toPairs(array.slice(2), 2));\n}\n"],
5
+ "mappings": "AAAA,OAAO,uBAAuB;AAEvB,gBAAS,UAAU,aAAa,aAAa;AAElD,QAAM,QAAQ,kBAAkB,6CAA6C;AAC7E,QAAM,YAAY,MAAM,OAAO,MAAM;AAGrC,QAAM,oBAAoB,YAAY,IAAI,gBAAc,WAAW,MAAM,KAAK,IAAI,IAAI,UAAU,MAAM,GAAG,UAAU,EAAE;AAGrH,QAAM,QAAQ,QAAQ,iBAAiB;AACvC,QAAM,UAAU,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,IAAI,GAAG,WAAW,IAAI,CAAC,OAAO,WAAW,IAAI,CAAC,KAAK,GAAG,WAAW,IAAI,CAAC,EAAE;AAE9G,YAAU,UAAU,MAAM,MAAM,MAAM,KAAK,UAAU,KAAK,CAAC,EAAE;AAC7D,YAAU,YAAY,QAAQ,MAAM,MAAM,KAAK,UAAU,OAAO,CAAC,EAAE;AAEnE,SAAO;AACT;AAEA,SAAS,QAAQ,OAAO;AACtB,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,CAAC;AAAA,EACV;AACA,SAAO,CAAC,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,OAAO,QAAQ,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC;AAC9D;",
6
+ "names": []
7
+ }
@@ -0,0 +1,75 @@
1
+ import createDebugLogger from "debug";
2
+ import createClient, { SruSearchError } from "@natlibfi/sru-client";
3
+ export class CandidateSearchError extends Error {
4
+ }
5
+ export default async function({ url, queryList, queryListType, maxCandidates = 50 }) {
6
+ const debug = createDebugLogger("@natlibfi/melinda-record-matching:candidate-search:choose-queries");
7
+ const debugData = debug.extend("data");
8
+ const debugDev = debug.extend("dev");
9
+ debugData(`Url: ${url}`);
10
+ debugData(`QueryList: ${queryList}`);
11
+ debugData(`queryListType: ${queryListType}`);
12
+ const client = createClient({
13
+ url,
14
+ maxRecordsPerRequest: 0,
15
+ version: "2.0",
16
+ retrieveAll: false
17
+ });
18
+ debugDev(`QueryList (type: ${queryListType}) ${JSON.stringify(queryList)}`);
19
+ try {
20
+ const { queriesWithTotals } = await getQueryTotals({ queryList, queryOffset: 0, queriesWithTotals: [] });
21
+ debugDev(`QueryResult: ${JSON.stringify(queriesWithTotals)}`);
22
+ const filteredQueryResult = filterQueryResult({ queriesWithTotals, maxCandidates });
23
+ debugDev(`filteredQueryResult: ${JSON.stringify(filteredQueryResult)}`);
24
+ return filteredQueryResult;
25
+ } catch (err) {
26
+ throw new CandidateSearchError(err);
27
+ }
28
+ async function getQueryTotals({ queryList: queryList2, queryOffset = 0, queriesWithTotals = [] }) {
29
+ const query = queryList2[queryOffset];
30
+ debug(`Running query ${JSON.stringify(query)} (${queryOffset}) for total`);
31
+ if (query) {
32
+ const { total } = await retrieveTotal();
33
+ const newQueriesWithTotals = [...queriesWithTotals, { query, total }];
34
+ debug(`Query ${queryOffset} ${query} done.`);
35
+ debug(`There are (${queryList2.length - (queryOffset + 1)} queries left)`);
36
+ return getQueryTotals({ queryList: queryList2, queryOffset: queryOffset + 1, queriesWithTotals: newQueriesWithTotals });
37
+ }
38
+ debug(`All ${queryList2.length} queries done, there's no query for ${queryOffset}`);
39
+ return { queriesWithTotals };
40
+ function retrieveTotal() {
41
+ return new Promise((resolve, reject) => {
42
+ let totalRecords = 0;
43
+ debug(`Searching total amount of candidates for query: ${query}`);
44
+ client.searchRetrieve(query).on("error", (err) => {
45
+ if (err instanceof SruSearchError) {
46
+ debug(`SRU SruSearchError for query: ${query}: ${err}`);
47
+ reject(new CandidateSearchError(`SRU SruSearchError for query: ${query}: ${err}`));
48
+ }
49
+ debug(`SRU error for query: ${query}: ${err}`);
50
+ reject(new CandidateSearchError(`SRU error for query: ${query}: ${err}`));
51
+ }).on("total", (total) => {
52
+ debug(`Got total: ${total}`);
53
+ totalRecords += total;
54
+ }).on("end", () => {
55
+ try {
56
+ resolve({ total: totalRecords });
57
+ } catch (err) {
58
+ debug(`Error caught on END`);
59
+ reject(err);
60
+ }
61
+ }).on("record", () => {
62
+ debugDev(`RECORD: We should no get records here`);
63
+ });
64
+ });
65
+ }
66
+ }
67
+ function filterQueryResult({ queriesWithTotals, maxCandidates: maxCandidates2 }) {
68
+ debug(`Filtering queries (${queriesWithTotals.length}), maxCandidates: ${maxCandidates2}`);
69
+ debugData(`${JSON.stringify(queriesWithTotals)}`);
70
+ const filteredQueryResult = queriesWithTotals.filter((queryWithTotal) => queryWithTotal.total !== 0 && queryWithTotal.total < maxCandidates2);
71
+ debugData(`${JSON.stringify(filteredQueryResult)}`);
72
+ return filteredQueryResult;
73
+ }
74
+ }
75
+ //# sourceMappingURL=choose-queries.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/candidate-search/choose-queries.js"],
4
+ "sourcesContent": ["import createDebugLogger from 'debug';\nimport createClient, {SruSearchError} from '@natlibfi/sru-client';\n\nexport class CandidateSearchError extends Error { }\n\nexport default async function ({url, queryList, queryListType, maxCandidates = 50}) {\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:choose-queries');\n const debugData = debug.extend('data');\n const debugDev = debug.extend('dev');\n\n debugData(`Url: ${url}`);\n debugData(`QueryList: ${queryList}`);\n debugData(`queryListType: ${queryListType}`);\n\n const client = createClient({\n url,\n maxRecordsPerRequest: 0,\n version: '2.0',\n retrieveAll: false\n });\n\n debugDev(`QueryList (type: ${queryListType}) ${JSON.stringify(queryList)}`);\n try {\n const {queriesWithTotals} = await getQueryTotals({queryList, queryOffset: 0, queriesWithTotals: []});\n debugDev(`QueryResult: ${JSON.stringify(queriesWithTotals)}`);\n const filteredQueryResult = filterQueryResult({queriesWithTotals, maxCandidates});\n debugDev(`filteredQueryResult: ${JSON.stringify(filteredQueryResult)}`);\n return filteredQueryResult;\n } catch (err) {\n throw new CandidateSearchError(err);\n }\n\n async function getQueryTotals({queryList, queryOffset = 0, queriesWithTotals = []}) {\n\n const query = queryList[queryOffset];\n debug(`Running query ${JSON.stringify(query)} (${queryOffset}) for total`);\n\n if (query) {\n const {total} = await retrieveTotal();\n\n const newQueriesWithTotals = [...queriesWithTotals, {query, total}];\n debug(`Query ${queryOffset} ${query} done.`);\n debug(`There are (${queryList.length - (queryOffset + 1)} queries left)`);\n return getQueryTotals({queryList, queryOffset: queryOffset + 1, queriesWithTotals: newQueriesWithTotals});\n }\n\n debug(`All ${queryList.length} queries done, there's no query for ${queryOffset}`);\n return {queriesWithTotals};\n\n function retrieveTotal() {\n return new Promise((resolve, reject) => {\n let totalRecords = 0;\n\n debug(`Searching total amount of candidates for query: ${query}`);\n\n client.searchRetrieve(query)\n .on('error', err => {\n if (err instanceof SruSearchError) {\n debug(`SRU SruSearchError for query: ${query}: ${err}`);\n reject(new CandidateSearchError(`SRU SruSearchError for query: ${query}: ${err}`));\n }\n debug(`SRU error for query: ${query}: ${err}`);\n reject(new CandidateSearchError(`SRU error for query: ${query}: ${err}`));\n })\n .on('total', total => {\n debug(`Got total: ${total}`);\n totalRecords += total;\n })\n .on('end', () => {\n try {\n resolve({total: totalRecords});\n } catch (err) {\n debug(`Error caught on END`);\n reject(err);\n }\n })\n .on('record', () => {\n debugDev(`RECORD: We should no get records here`);\n });\n });\n }\n }\n function filterQueryResult({queriesWithTotals, maxCandidates}) {\n debug(`Filtering queries (${queriesWithTotals.length}), maxCandidates: ${maxCandidates}`);\n debugData(`${JSON.stringify(queriesWithTotals)}`);\n // Drop queries where total result is 0 or greater than given maxCandidates\n const filteredQueryResult = queriesWithTotals.filter((queryWithTotal) => queryWithTotal.total !== 0 && queryWithTotal.total < maxCandidates);\n debugData(`${JSON.stringify(filteredQueryResult)}`);\n return filteredQueryResult;\n }\n\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["queryList", "maxCandidates"]
7
+ }
@@ -0,0 +1,118 @@
1
+ import createDebugLogger from "debug";
2
+ import createClient, { SruSearchError } from "@natlibfi/sru-client";
3
+ import { MarcRecord } from "@natlibfi/marc-record";
4
+ import generateQueryList from "./query-list/index.js";
5
+ import chooseQueries from "./choose-queries.js";
6
+ export { searchTypes } from "./query-list/index.js";
7
+ export class CandidateSearchError extends Error {
8
+ }
9
+ export default async ({ record, searchSpec, url, maxCandidates, maxRecordsPerRequest = 50, serverMaxResult = 2e4 }) => {
10
+ MarcRecord.setValidationOptions({ subfieldValues: false });
11
+ const debug = createDebugLogger("@natlibfi/melinda-record-matching:candidate-search");
12
+ const debugData = debug.extend("data");
13
+ debugData(`SearchSpec: ${JSON.stringify(searchSpec)}`);
14
+ debugData(`Url: ${url}`);
15
+ debugData(`MaxRecordsPerRequest ${maxRecordsPerRequest}`);
16
+ debugData(`ServerMaxResult: ${serverMaxResult}`);
17
+ debugData(`MaxCandidates: ${maxCandidates}`);
18
+ const adjustedMaxRecordsPerRequest = maxRecordsPerRequest >= maxCandidates ? maxCandidates : maxRecordsPerRequest;
19
+ const client = createClient({
20
+ url,
21
+ maxRecordsPerRequest: adjustedMaxRecordsPerRequest,
22
+ version: "2.0",
23
+ retrieveAll: false,
24
+ metadataFormat: "marcJson"
25
+ });
26
+ const inputRecordId = getRecordId(record);
27
+ const queryListResult = await generateQueryList(record, searchSpec, client);
28
+ const queryList = queryListResult[0]?.queryList ? queryListResult[0].queryList : queryListResult;
29
+ const queryListType = queryListResult[0]?.queryListType ? queryListResult[0].queryListType : void 0;
30
+ if (queryList.length === 0) {
31
+ debug(`Empty list`);
32
+ throw new CandidateSearchError(`Generated query list contains no queries`);
33
+ }
34
+ if (queryListType && queryListType !== "alternates") {
35
+ debug(`Unknown queryListType`);
36
+ throw new CandidateSearchError(`Generated query list has invalid type`);
37
+ }
38
+ debug(`Searching matches for ${inputRecordId}`);
39
+ const chosenQueryList = await filterQueryList({ queryList, queryListType });
40
+ debug(`Chosen queries: ${JSON.stringify(chosenQueryList)}`);
41
+ async function filterQueryList({ queryList: queryList2, queryListType: queryListType2, maxCandidates: maxCandidates2 }) {
42
+ debug(`Generated queryList (type: ${queryListType2}) ${JSON.stringify(queryList2)}`);
43
+ if (queryListType2 === "alternates" && queryList2.length > 1) {
44
+ const queryListResult2 = await chooseQueries({ url, queryList: queryList2, queryListType: queryListType2, maxCandidates: maxCandidates2 });
45
+ debug(`queryListResult: ${JSON.stringify(queryListResult2)}`);
46
+ return queryListResult2.map((elem) => elem.query);
47
+ }
48
+ return queryList2;
49
+ }
50
+ return { search };
51
+ async function search({ queryOffset = 0, resultSetOffset = 1, totalRecords = 0, searchCounter = 0, queryCandidateCounter = 0, queryCounter = 0, maxedQueries = [] }) {
52
+ const query = chosenQueryList[queryOffset];
53
+ debug(`Running query ${JSON.stringify(query)} (${queryOffset})`);
54
+ if (query) {
55
+ const { records, nextOffset, total } = await retrieveRecords(client, query, resultSetOffset);
56
+ debugData(`ResultSetOffset: ${resultSetOffset}`);
57
+ const newTotalRecords = resultSetOffset === 1 ? total : totalRecords;
58
+ const newQueryCounter = resultSetOffset === 1 ? queryCounter + 1 : queryCounter;
59
+ const newSearchCounter = resultSetOffset === 1 ? 1 : searchCounter + 1;
60
+ const newQueryCandidateCounter = resultSetOffset === 1 ? records.length : queryCandidateCounter + records.length;
61
+ const maxedQuery = resultSetOffset === 1 ? checkMaxedQuery(query, total, serverMaxResult) : void 0;
62
+ const newMaxedQueries = maxedQuery ? maxedQueries.concat(maxedQuery) : maxedQueries;
63
+ if (typeof nextOffset === "number") {
64
+ debug(`Next search will be for query ${queryOffset} ${query}, starting from record ${nextOffset}`);
65
+ return { records, queryOffset, resultSetOffset: nextOffset, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCounter, maxedQueries: newMaxedQueries };
66
+ }
67
+ debug(`Query ${queryOffset} ${query} done.`);
68
+ debug(`There are (${queryList.length - (queryOffset + 1)} queries left)`);
69
+ return { records, queryOffset: queryOffset + 1, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCounter, maxedQueries: newMaxedQueries };
70
+ }
71
+ debug(`All ${queryList.length} queries done, there's no query for ${queryOffset}`);
72
+ return { records: [], queriesLeft: 0, queryCounter, maxedQueries };
73
+ }
74
+ function checkMaxedQuery(query, total, serverMaxResult2) {
75
+ if (total >= serverMaxResult2) {
76
+ debug(`WARNING: Query ${query} resulted in ${total} hits which meets the serverMaxResult (${serverMaxResult2}) `);
77
+ return query;
78
+ }
79
+ }
80
+ function getRecordId(record2) {
81
+ const [field] = record2.get(/^001$/u);
82
+ return field ? field.value : "";
83
+ }
84
+ };
85
+ export function retrieveRecords(client, query, resultSetOffset) {
86
+ const debug = createDebugLogger("@natlibfi/melinda-record-matching:candidate-search:retrieveRecords");
87
+ const debugData = debug.extend("data");
88
+ return new Promise((resolve, reject) => {
89
+ const records = [];
90
+ let totalRecords = 0;
91
+ debug(`Searching for candidates with query: ${query} (Offset ${resultSetOffset})`);
92
+ client.searchRetrieve(query, { startRecord: resultSetOffset }).on("error", (err) => {
93
+ if (err instanceof SruSearchError) {
94
+ debug(`SRU SruSearchError for query: ${query}: ${err}`);
95
+ reject(new CandidateSearchError(`SRU SruSearchError for query: ${query}: ${err}`));
96
+ }
97
+ debug(`SRU error for query: ${query}: ${err}`);
98
+ reject(new CandidateSearchError(`SRU error for query: ${query}: ${err}`));
99
+ }).on("total", (total) => {
100
+ debug(`Got total: ${total}`);
101
+ totalRecords += total;
102
+ }).on("end", (nextOffset) => {
103
+ try {
104
+ debug(`Found ${records.length} records`);
105
+ resolve({ nextOffset, records, total: totalRecords });
106
+ } catch (err) {
107
+ debug(`Error caught on END`);
108
+ reject(err);
109
+ }
110
+ }).on("record", (record) => {
111
+ const [field] = record.get(/^001$/u);
112
+ debug(field);
113
+ const id = field.value ? field.value : "";
114
+ records.push({ record, id });
115
+ });
116
+ });
117
+ }
118
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/candidate-search/index.js"],
4
+ "sourcesContent": ["import createDebugLogger from 'debug';\nimport createClient, {SruSearchError} from '@natlibfi/sru-client';\nimport {MarcRecord} from '@natlibfi/marc-record';\n\nimport generateQueryList from './query-list/index.js';\nimport chooseQueries from './choose-queries.js';\n\nexport {searchTypes} from './query-list/index.js';\n\nexport class CandidateSearchError extends Error { }\n\n// serverMaxResults : maximum size of total search result available from the server, defaults to Aleph's 20000\n\nexport default async ({record, searchSpec, url, maxCandidates, maxRecordsPerRequest = 50, serverMaxResult = 20000}) => {\n MarcRecord.setValidationOptions({subfieldValues: false});\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search');\n const debugData = debug.extend('data');\n\n debugData(`SearchSpec: ${JSON.stringify(searchSpec)}`);\n debugData(`Url: ${url}`);\n debugData(`MaxRecordsPerRequest ${maxRecordsPerRequest}`);\n debugData(`ServerMaxResult: ${serverMaxResult}`);\n debugData(`MaxCandidates: ${maxCandidates}`);\n\n // Do not retrieve more candidates than defined in maxCandidates\n const adjustedMaxRecordsPerRequest = maxRecordsPerRequest >= maxCandidates ? maxCandidates : maxRecordsPerRequest;\n\n const client = createClient({\n url,\n maxRecordsPerRequest: adjustedMaxRecordsPerRequest,\n version: '2.0',\n retrieveAll: false,\n metadataFormat: 'marcJson'\n });\n\n const inputRecordId = getRecordId(record);\n const queryListResult = await generateQueryList(record, searchSpec, client);\n const queryList = queryListResult[0]?.queryList ? queryListResult[0].queryList : queryListResult;\n const queryListType = queryListResult[0]?.queryListType ? queryListResult[0].queryListType : undefined;\n\n // if generateQueryList errored we should throw 422\n if (queryList.length === 0) {\n debug(`Empty list`);\n throw new CandidateSearchError(`Generated query list contains no queries`);\n }\n if (queryListType && queryListType !== 'alternates') {\n debug(`Unknown queryListType`);\n throw new CandidateSearchError(`Generated query list has invalid type`);\n }\n\n debug(`Searching matches for ${inputRecordId}`);\n const chosenQueryList = await filterQueryList({queryList, queryListType});\n debug(`Chosen queries: ${JSON.stringify(chosenQueryList)}`);\n\n async function filterQueryList({queryList, queryListType, maxCandidates}) {\n debug(`Generated queryList (type: ${queryListType}) ${JSON.stringify(queryList)}`);\n\n if (queryListType === 'alternates' && queryList.length > 1) {\n const queryListResult = await chooseQueries({url, queryList, queryListType, maxCandidates});\n debug(`queryListResult: ${JSON.stringify(queryListResult)}`);\n return queryListResult.map(elem => elem.query);\n }\n return queryList;\n }\n // state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)\n // state.query : current query (undefined if there was no queries left)\n // state.searchCounter : sequence for current search for current query (undefined, if there we no queries left)\n // state.queryCandidateCounter: amount of candidates (records+failures) retrieved from SRU for matching for current query, including the current record+failure set (undefined if there were no queries left)\n // state.queriesLeft : amount of queries left\n // state.queryCounter : sequence for current query\n // state.maxedQueries : queries that resulted in more than serverMaxResults hits\n\n return {search};\n\n // eslint-disable-next-line max-statements\n async function search({queryOffset = 0, resultSetOffset = 1, totalRecords = 0, searchCounter = 0, queryCandidateCounter = 0, queryCounter = 0, maxedQueries = []}) {\n const query = chosenQueryList[queryOffset];\n debug(`Running query ${JSON.stringify(query)} (${queryOffset})`);\n\n if (query) {\n const {records, nextOffset, total} = await retrieveRecords(client, query, resultSetOffset);\n\n // If resultSetOffset === 1 this is the first search for the current query\n debugData(`ResultSetOffset: ${resultSetOffset}`);\n const newTotalRecords = resultSetOffset === 1 ? total : totalRecords;\n const newQueryCounter = resultSetOffset === 1 ? queryCounter + 1 : queryCounter;\n const newSearchCounter = resultSetOffset === 1 ? 1 : searchCounter + 1;\n const newQueryCandidateCounter = resultSetOffset === 1 ? records.length : queryCandidateCounter + records.length;\n\n const maxedQuery = resultSetOffset === 1 ? checkMaxedQuery(query, total, serverMaxResult) : undefined;\n const newMaxedQueries = maxedQuery ? maxedQueries.concat(maxedQuery) : maxedQueries;\n\n if (typeof nextOffset === 'number') {\n debug(`Next search will be for query ${queryOffset} ${query}, starting from record ${nextOffset}`);\n return {records, queryOffset, resultSetOffset: nextOffset, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCounter, maxedQueries: newMaxedQueries};\n }\n debug(`Query ${queryOffset} ${query} done.`);\n debug(`There are (${queryList.length - (queryOffset + 1)} queries left)`);\n return {records, queryOffset: queryOffset + 1, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCounter, maxedQueries: newMaxedQueries};\n }\n\n debug(`All ${queryList.length} queries done, there's no query for ${queryOffset}`);\n return {records: [], queriesLeft: 0, queryCounter, maxedQueries};\n }\n\n function checkMaxedQuery(query, total, serverMaxResult) {\n if (total >= serverMaxResult) {\n debug(`WARNING: Query ${query} resulted in ${total} hits which meets the serverMaxResult (${serverMaxResult}) `);\n return query;\n }\n }\n\n function getRecordId(record) {\n const [field] = record.get(/^001$/u);\n return field ? field.value : '';\n }\n};\n\nexport function retrieveRecords(client, query, resultSetOffset) {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:retrieveRecords');\n // eslint-disable-next-line no-unused-vars\n const debugData = debug.extend('data');\n\n return new Promise((resolve, reject) => {\n const records = [];\n let totalRecords = 0;\n\n debug(`Searching for candidates with query: ${query} (Offset ${resultSetOffset})`);\n\n client.searchRetrieve(query, {startRecord: resultSetOffset})\n .on('error', err => {\n if (err instanceof SruSearchError) {\n debug(`SRU SruSearchError for query: ${query}: ${err}`);\n reject(new CandidateSearchError(`SRU SruSearchError for query: ${query}: ${err}`));\n }\n\n debug(`SRU error for query: ${query}: ${err}`);\n reject(new CandidateSearchError(`SRU error for query: ${query}: ${err}`));\n })\n .on('total', total => {\n debug(`Got total: ${total}`);\n totalRecords += total;\n })\n .on('end', nextOffset => {\n try {\n debug(`Found ${records.length} records`);\n\n resolve({nextOffset, records, total: totalRecords});\n } catch (err) {\n debug(`Error caught on END`);\n reject(err);\n }\n })\n .on('record', record => {\n const [field] = record.get(/^001$/u);\n debug(field);\n const id = field.value ? field.value : '';\n records.push({record, id});\n });\n });\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["queryList", "queryListType", "maxCandidates", "queryListResult", "serverMaxResult", "record"]
7
+ }
@@ -0,0 +1,92 @@
1
+ import assert from "node:assert";
2
+ import { describe } from "node:test";
3
+ import { READERS } from "@natlibfi/fixura";
4
+ import generateTests from "@natlibfi/fixugen-http-client";
5
+ import { MarcRecord } from "@natlibfi/marc-record";
6
+ import { Error as MatchingError } from "@natlibfi/melinda-commons";
7
+ import createSearchInterface, { CandidateSearchError } from "./index.js";
8
+ import createDebugLogger from "debug";
9
+ const debug = createDebugLogger("@natlibfi/melinda-record-matching:candidate-search:test");
10
+ describe("candidate-search", () => {
11
+ generateTests({
12
+ callback,
13
+ path: [import.meta.dirname, "..", "..", "test-fixtures", "candidate-search", "index"],
14
+ recurse: false,
15
+ fixura: {
16
+ reader: READERS.JSON
17
+ }
18
+ });
19
+ async function callback({ getFixture, factoryOptions, searchOptions, expectedFactoryError = false, expectedSearchError = false, enabled = true }) {
20
+ const url = "http://foo.bar";
21
+ if (!enabled) {
22
+ return;
23
+ }
24
+ if (expectedFactoryError) {
25
+ debug(`We're expecting an error`);
26
+ if (expectedFactoryError.isCandidateSearchError) {
27
+ try {
28
+ const result = await createSearchInterface({ ...formatFactoryOptions(), url });
29
+ debug(result);
30
+ } catch (err) {
31
+ assert.equal(err instanceof CandidateSearchError, true);
32
+ assert.match(err.message, new RegExp(expectedFactoryError.message));
33
+ } finally {
34
+ return;
35
+ }
36
+ }
37
+ try {
38
+ const result = await createSearchInterface({ ...formatFactoryOptions(), url });
39
+ debug(result);
40
+ } catch (err) {
41
+ assert.equal(err instanceof Error, true);
42
+ assert.match(err.message, new RegExp(expectedFactoryError.message));
43
+ } finally {
44
+ return;
45
+ }
46
+ }
47
+ const { search } = await createSearchInterface({ ...formatFactoryOptions(), url });
48
+ await iterate({ searchOptions, expectedSearchError });
49
+ function formatFactoryOptions() {
50
+ debug(`Using factoryOptions: ${JSON.stringify(factoryOptions)}`);
51
+ return {
52
+ ...factoryOptions,
53
+ maxRecordsPerRequest: factoryOptions.maxRecordsPerRequest || 1,
54
+ maxServerResults: factoryOptions.maxServerResults || void 0,
55
+ record: new MarcRecord(factoryOptions.record, { subfieldValues: false })
56
+ };
57
+ }
58
+ async function iterate({ searchOptions: searchOptions2, expectedSearchError: expectedSearchError2, expectedErrorStatus, count = 1 }) {
59
+ const expectedResults = getFixture(`expectedResults${count}.json`);
60
+ if (expectedSearchError2) {
61
+ try {
62
+ await search(searchOptions2);
63
+ throw new Error("Expected an error");
64
+ } catch (err) {
65
+ debug(`Got an error: ${err}`);
66
+ assert(err instanceof Error);
67
+ const errorMessage = err instanceof MatchingError ? err.payload.message : err.message;
68
+ const errorStatus = err instanceof MatchingError ? err.status : void 0;
69
+ debug(`errorMessage: ${errorMessage}, errorStatus: ${errorStatus}`);
70
+ assert.match(errorMessage, new RegExp(expectedSearchError2, "u"));
71
+ if (expectedErrorStatus) {
72
+ assert.equal(errorStatus, expectedErrorStatus);
73
+ return;
74
+ }
75
+ return;
76
+ }
77
+ }
78
+ if (!expectedSearchError2) {
79
+ const results = await search(searchOptions2);
80
+ assert.deepStrictEqual(formatResults(results), expectedResults);
81
+ }
82
+ function formatResults(results) {
83
+ debug(results);
84
+ return {
85
+ ...results,
86
+ records: results.records.map(({ record, id }) => ({ id, record: record.toObject() }))
87
+ };
88
+ }
89
+ }
90
+ }
91
+ });
92
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/candidate-search/index.test.js"],
4
+ "sourcesContent": ["import assert from 'node:assert';\nimport {describe} from 'node:test';\nimport {READERS} from '@natlibfi/fixura';\nimport generateTests from '@natlibfi/fixugen-http-client';\nimport {MarcRecord} from '@natlibfi/marc-record';\nimport {Error as MatchingError} from '@natlibfi/melinda-commons';\nimport createSearchInterface, {CandidateSearchError} from './index.js';\nimport createDebugLogger from 'debug';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:test');\n\ndescribe('candidate-search', () => {\n generateTests({\n callback,\n path: [import.meta.dirname, '..', '..', 'test-fixtures', 'candidate-search', 'index'],\n recurse: false,\n fixura: {\n reader: READERS.JSON\n }\n });\n\n // eslint-disable-next-line max-statements\n async function callback({getFixture, factoryOptions, searchOptions, expectedFactoryError = false, expectedSearchError = false, enabled = true}) {\n const url = 'http://foo.bar';\n\n if (!enabled) {\n return;\n }\n\n if (expectedFactoryError) {\n debug(`We're expecting an error`);\n if (expectedFactoryError.isCandidateSearchError) {\n try {\n const result = await createSearchInterface({...formatFactoryOptions(), url});\n debug(result);\n } catch (err) {\n assert.equal(err instanceof CandidateSearchError, true);\n assert.match(err.message, new RegExp(expectedFactoryError.message));\n } finally {\n return;\n }\n }\n\n try {\n const result = await createSearchInterface({...formatFactoryOptions(), url});\n debug(result);\n } catch (err) {\n assert.equal(err instanceof Error, true);\n assert.match(err.message, new RegExp(expectedFactoryError.message));\n } finally {\n return;\n }\n }\n\n const {search} = await createSearchInterface({...formatFactoryOptions(), url});\n await iterate({searchOptions, expectedSearchError});\n\n function formatFactoryOptions() {\n debug(`Using factoryOptions: ${JSON.stringify(factoryOptions)}`);\n return {\n ...factoryOptions,\n maxRecordsPerRequest: factoryOptions.maxRecordsPerRequest || 1,\n maxServerResults: factoryOptions.maxServerResults || undefined,\n record: new MarcRecord(factoryOptions.record, {subfieldValues: false})\n };\n }\n\n async function iterate({searchOptions, expectedSearchError, expectedErrorStatus, count = 1}) {\n const expectedResults = getFixture(`expectedResults${count}.json`);\n\n if (expectedSearchError) {\n try {\n await search(searchOptions);\n throw new Error('Expected an error');\n } catch (err) {\n debug(`Got an error: ${err}`);\n assert(err instanceof Error);\n const errorMessage = err instanceof MatchingError ? err.payload.message : err.message;\n const errorStatus = err instanceof MatchingError ? err.status : undefined;\n debug(`errorMessage: ${errorMessage}, errorStatus: ${errorStatus}`);\n assert.match(errorMessage, new RegExp(expectedSearchError, 'u'));\n\n if (expectedErrorStatus) {\n assert.equal(errorStatus, expectedErrorStatus);\n return;\n }\n return;\n }\n }\n\n if (!expectedSearchError) {\n const results = await search(searchOptions);\n assert.deepStrictEqual(formatResults(results), expectedResults);\n }\n\n function formatResults(results) {\n debug(results);\n return {\n ...results,\n records: results.records.map(({record, id}) => ({id, record: record.toObject()}))\n };\n }\n }\n }\n});\n"],
5
+ "mappings": "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",
6
+ "names": ["searchOptions", "expectedSearchError"]
7
+ }
@@ -0,0 +1,260 @@
1
+ import createDebugLogger from "debug";
2
+ import { toQueries } from "../candidate-search-utils.js";
3
+ import { getMelindaIdsF035, validateSidFieldSubfieldCounts, getSubfieldValues, testStringOrNumber } from "../../matching-utils.js";
4
+ const debug = createDebugLogger("@natlibfi/melinda-record-matching:candidate-search:query");
5
+ export function bibSourceIds(record) {
6
+ const debug2 = createDebugLogger("@natlibfi/melinda-record-matching:candidate-search:query:source-ids");
7
+ const debugData = debug2.extend("data");
8
+ debug2(`Creating queries for sourceid's`);
9
+ const fSids = record.get("SID");
10
+ debugData(`SID-fields (${fSids.length}): ${JSON.stringify(fSids)}`);
11
+ return fSids.length > 0 ? toSidQueries(fSids) : [];
12
+ function toSidQueries(fSids2) {
13
+ debug2(`Creating actual queries for sourceid's`);
14
+ const sidStrings = getSidStrings(fSids2);
15
+ if (sidStrings.length < 1) {
16
+ debug2(`No identifiers found, no queries created.`);
17
+ return [];
18
+ }
19
+ const sidQueries = toQueries(sidStrings, "melinda.sourceid");
20
+ return sidQueries;
21
+ function getSidStrings(fSids3) {
22
+ debug2(`Getting Sid strings from SID fields`);
23
+ const sidStrings2 = fSids3.map(toSidString).filter((nonEmptySid) => nonEmptySid);
24
+ return sidStrings2;
25
+ function toSidString(field) {
26
+ debug2(`Getting string from a field`);
27
+ return validateSidFieldSubfieldCounts(field) ? createSidString(field) : "";
28
+ function createSidString(field2) {
29
+ debug2(`Creating string from a field`);
30
+ const [sfC] = getSubfieldValues(field2, "c");
31
+ const [sfB] = getSubfieldValues(field2, "b");
32
+ const cleanedSfC = removeSourcePrefix(normalizeSidSubfieldValue(sfC));
33
+ const cleanedSfB = normalizeSidSubfieldValue(sfB);
34
+ debugData(`${JSON.stringify(sfC)} + ${JSON.stringify(sfB)}`);
35
+ return cleanedSfC.concat(cleanedSfB);
36
+ }
37
+ function removeSourcePrefix(subfieldValue) {
38
+ const sourcePrefixRegex = /^(?<sourcePrefix>\([A-Za-z0-9-]+\))(?<id>.+)$/u;
39
+ const normalizedValue = testStringOrNumber(subfieldValue) ? String(subfieldValue).replace(sourcePrefixRegex, "$<id>") : "";
40
+ debugData(`Normalized ${subfieldValue} to ${normalizedValue}`);
41
+ return normalizedValue;
42
+ }
43
+ function normalizeSidSubfieldValue(subfieldValue) {
44
+ debugData(`Normalizing ${subfieldValue}`);
45
+ const normalizeAwayRegex = /[- ]/u;
46
+ return testStringOrNumber(subfieldValue) ? String(subfieldValue).replace(normalizeAwayRegex, "") : "";
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ export function bibMelindaIds(record) {
53
+ const debug2 = createDebugLogger("@natlibfi/melinda-record-matching:candidate-search:query:bibMelindaIds");
54
+ const debugData = debug2.extend("data");
55
+ debug2(`Creating queries for MelindaIds`);
56
+ const melindaIds = getMelindaIdsF035(record);
57
+ debugData(`Unique identifiers (${melindaIds.length}): ${JSON.stringify(melindaIds)}`);
58
+ if (melindaIds.length < 1) {
59
+ debug2(`No identifiers found, no queries created.`);
60
+ return [];
61
+ }
62
+ return toQueries(melindaIds, "melinda.melindaid");
63
+ }
64
+ export function bibHostComponents(record) {
65
+ const debug2 = createDebugLogger("@natlibfi/melinda-record-matching:candidate-search:query:bibHostComponents");
66
+ const debugData = debug2.extend("data");
67
+ debug2(`Creating queries for hostIds`);
68
+ const id = getHostId();
69
+ debugData(`Found id: ${JSON.stringify(id)}`);
70
+ return testStringOrNumber(id) ? [`melinda.partsofhost=${id}`] : [];
71
+ function getHostId() {
72
+ const [field] = record.get(/^773$/u);
73
+ if (field) {
74
+ const { value } = field.subfields.find(({ code }) => code === "w") || {};
75
+ if (testStringOrNumber(value) && /^\(FI-MELINDA\)/u.test(String(value))) {
76
+ return String(value).replace(/^\(FI-MELINDA\)/u, "");
77
+ }
78
+ if (testStringOrNumber(value) && /^\(FIN01\)/u.test(String(value))) {
79
+ return String(value).replace(/^\(FIN01\)/u, "");
80
+ }
81
+ return false;
82
+ }
83
+ return false;
84
+ }
85
+ }
86
+ export function bibTitle(record) {
87
+ return bibTitleAuthorPublisher({ record, onlyTitleLength: 5 });
88
+ }
89
+ export function bibTitleAuthor(record) {
90
+ debug("bibTitleAuthor");
91
+ return bibTitleAuthorPublisher({ record, onlyTitleLength: 100 });
92
+ }
93
+ export function bibTitleAuthorYear(record) {
94
+ debug("bibTitleAuthorYearAlternates");
95
+ return bibTitleAuthorPublisher({ record, onlyTitleLength: 100, addYear: true });
96
+ }
97
+ export function bibTitleAuthorYearAlternates(record) {
98
+ debug("bibTitleAuthorYearAlternates");
99
+ const origQueryList = bibTitleAuthorPublisher({ record, onlyTitleLength: 100, addYear: true, alternates: true, alternateQueries: [] });
100
+ debug(`${JSON.stringify(origQueryList)}`);
101
+ return { queryList: Array.from(origQueryList).reverse(), queryListType: "alternates" };
102
+ }
103
+ export function bibTitleAuthorPublisher({ record, onlyTitleLength, addYear = false, alternates = false, alternateQueries = [] }) {
104
+ debug(`bibTitleAuthorPublisher, onlyTitleLength: ${onlyTitleLength}, addYear: ${addYear}, alternates: ${alternates}`);
105
+ const title = getTitle();
106
+ if (testStringOrNumber(title)) {
107
+ const formatted = String(title).replace(/[^\w\s\p{Alphabetic}]/gu, "").replace(/ +/gu, " ").trim().slice(0, 30).trim();
108
+ const useWordSearch = checkUseWordSearch(formatted);
109
+ if (formatted.length >= onlyTitleLength && !alternates) {
110
+ return [`dc.title="${useWordSearch ? "" : "^"}${formatted}*"`];
111
+ }
112
+ const queryIsOkAlone = formatted.length >= 5;
113
+ const query = `dc.title="${useWordSearch || !queryIsOkAlone ? "" : "^"}${formatted}${queryIsOkAlone ? "*" : ""}"`;
114
+ debug(`query: ${query}`);
115
+ const newAlternateQueries = alternates ? [...alternateQueries, query] : alternateQueries;
116
+ return addAuthorsToSearch({ query, queryIsOkAlone, addYear, alternates, alternateQueries: newAlternateQueries });
117
+ }
118
+ return [];
119
+ function addAuthorsToSearch({ query, queryIsOkAlone = false, addYear: addYear2 = false, alternates: alternates2 = false, alternateQueries: alternateQueries2 = [] }) {
120
+ debug("addAuthorsToSearch");
121
+ const [authorQuery] = bibAuthors(record);
122
+ if (authorQuery !== void 0) {
123
+ if (addYear2) {
124
+ const newAlternateQueries = alternates2 ? [...alternateQueries2, `${authorQuery} AND ${query}`] : alternateQueries2;
125
+ return addYearToSearch({ query: `${authorQuery} AND ${query}`, queryIsOkAlone: true, alternates: alternates2, alternateQueries: newAlternateQueries });
126
+ }
127
+ return alternates2 ? alternateQueries2 : [`${authorQuery} AND ${query}`];
128
+ }
129
+ return addPublisherToSearch({ query, queryIsOkAlone, addYear: addYear2, alternates: alternates2, alternateQueries: alternateQueries2 });
130
+ }
131
+ function addPublisherToSearch({ query, queryIsOkAlone = false, addYear: addYear2 = false, alternates: alternates2 = false, alternateQueries: alternateQueries2 = [] }) {
132
+ const [publisherQuery] = bibPublishers(record);
133
+ if (publisherQuery !== void 0) {
134
+ if (addYear2) {
135
+ const newAlternateQueries = alternates2 ? [...alternateQueries2, `${publisherQuery} AND ${query}`] : alternateQueries2;
136
+ return addYearToSearch({ query: `${publisherQuery} AND ${query}`, queryIsOkAlone: true, alternates: alternates2, alternateQueries: newAlternateQueries });
137
+ }
138
+ return alternates2 ? alternateQueries2 : [`${publisherQuery} AND ${query}`];
139
+ }
140
+ if (queryIsOkAlone && !addYear2) {
141
+ return alternates2 ? alternateQueries2 : [`${query}`];
142
+ }
143
+ return addYearToSearch({ query, queryIsOkAlone, alternates: alternates2, alternateQueries: alternateQueries2 });
144
+ }
145
+ function addYearToSearch({ query, queryIsOkAlone = false, alternates: alternates2 = false, alternateQueries: alternateQueries2 = [] }) {
146
+ const [yearQuery] = bibYear(record);
147
+ if (yearQuery !== void 0) {
148
+ const newAlternateQueries = alternates2 ? [...alternateQueries2, `${yearQuery} AND ${query}`] : alternateQueries2;
149
+ return alternates2 ? newAlternateQueries : [`${yearQuery} AND ${query}`];
150
+ }
151
+ if (queryIsOkAlone) {
152
+ return alternates2 ? alternateQueries2 : [`${query}`];
153
+ }
154
+ return [];
155
+ }
156
+ function getTitle() {
157
+ const [field] = record.get(/^245$/u);
158
+ if (field) {
159
+ const titleString = field.subfields.filter(({ code }) => ["a", "b"].includes(code)).map(({ value }) => testStringOrNumber(value) ? String(value) : "").filter((value) => value).join(" ");
160
+ return titleString;
161
+ }
162
+ return false;
163
+ }
164
+ }
165
+ export function bibAuthors(record) {
166
+ const debug2 = createDebugLogger("@natlibfi/melinda-record-matching:candidate-search:query:bibAuthors");
167
+ const debugData = debug2.extend("data");
168
+ debug2(`Creating query for the first author`);
169
+ const author = getAuthor(record);
170
+ if (testStringOrNumber(author)) {
171
+ const formatted = String(author).replace(/[^\w\s\p{Alphabetic}]/gu, "").replace(/ +/gu, " ").trim().slice(0, 30).trim();
172
+ const useWordSearch = checkUseWordSearch(formatted);
173
+ debugData(`Author string: ${formatted}`);
174
+ if (formatted.length >= 5) {
175
+ return [`dc.author="${useWordSearch ? "" : "^"}${formatted}*"`];
176
+ }
177
+ return [];
178
+ }
179
+ return [];
180
+ function getAuthor(record2) {
181
+ const [field] = record2.get(/^(100)|(110)|(111)|(700)|(710)|(711)$/u);
182
+ if (field) {
183
+ const authorString = field.subfields.filter(({ code }) => ["a"].includes(code)).map(({ value }) => testStringOrNumber(value) ? String(value) : "").filter((value) => value).join(" ");
184
+ return authorString;
185
+ }
186
+ return false;
187
+ }
188
+ }
189
+ export function bibPublishers(record) {
190
+ const debug2 = createDebugLogger("@natlibfi/melinda-record-matching:candidate-search:query:bibPublishers");
191
+ const debugData = debug2.extend("data");
192
+ debug2(`Creating query for the first publisher`);
193
+ const publisher = getPublisher(record);
194
+ if (testStringOrNumber(publisher)) {
195
+ const formatted = String(publisher).replace(/[^\w\s\p{Alphabetic}]/gu, "").replace(/ +/gu, " ").trim().slice(0, 30).trim();
196
+ debugData(`Publisher string: ${formatted}`);
197
+ return [`dc.publisher="${formatted}"`];
198
+ }
199
+ return [];
200
+ function getPublisher(record2) {
201
+ const [field] = record2.get(/^(?:260)|(?:264)$/u);
202
+ if (field) {
203
+ const publisherString = field.subfields.filter(({ code }) => ["b"].includes(code)).map(({ value }) => testStringOrNumber(value) ? String(value) : "").filter((value) => value).join(" ");
204
+ return publisherString;
205
+ }
206
+ return false;
207
+ }
208
+ }
209
+ export function bibYear(record) {
210
+ const debug2 = createDebugLogger("@natlibfi/melinda-record-matching:candidate-search:query:bibYear");
211
+ const debugData = debug2.extend("data");
212
+ debug2(`Creating query for the publishing year`);
213
+ const year = getYear(record);
214
+ if (year) {
215
+ return [`dc.date="${year}"`];
216
+ }
217
+ return [];
218
+ function getYear(record2) {
219
+ const [f008] = record2.get(/^008$/u);
220
+ if (f008 === void 0) {
221
+ debug2("f008 missing");
222
+ return false;
223
+ }
224
+ debugData(`f008: ${JSON.stringify(f008)}`);
225
+ const { value } = f008;
226
+ return testStringOrNumber(value) ? String(value).slice(7, 11) : void 0;
227
+ }
228
+ }
229
+ export function checkUseWordSearch(formatted) {
230
+ const booleanStartWords = ["and ", "or ", "nor ", "not "];
231
+ return booleanStartWords.some((word) => formatted.toLowerCase().startsWith(word));
232
+ }
233
+ export function bibStandardIdentifiers(record) {
234
+ const debug2 = createDebugLogger("@natlibfi/melinda-record-matching:candidate-search:query:bibStandardIdentifiers");
235
+ const debugData = debug2.extend("data");
236
+ debug2(`Creating queries for standard identifiers`);
237
+ const fields = record.get(/^(?<def>020|022|024)$/u);
238
+ const identifiers = [].concat(...fields.map(toIdentifiers));
239
+ const uniqueIdentifiers = [...new Set(identifiers)];
240
+ debugData(`Standard identifier fields: ${JSON.stringify(fields)}`);
241
+ debugData(`Identifiers (${identifiers.length}): ${JSON.stringify(identifiers)}`);
242
+ debugData(`Unique identifiers (${uniqueIdentifiers.length}): ${JSON.stringify(uniqueIdentifiers)}`);
243
+ if (uniqueIdentifiers.length < 1) {
244
+ debug2(`No identifiers found, no queries created.`);
245
+ return [];
246
+ }
247
+ return toQueries(uniqueIdentifiers, "dc.identifier");
248
+ function toIdentifiers({ tag, subfields }) {
249
+ const issnIsbnReqExp = /^[A-Za-z0-9-]+$/u;
250
+ const otherIdReqExp = /^[A-Za-z0-9-:]+$/u;
251
+ if (tag === "022") {
252
+ return subfields.filter((sub) => ["a", "z", "y"].includes(sub.code) && testStringOrNumber(sub.value) && issnIsbnReqExp.test(String(sub.value))).map(({ value }) => String(value));
253
+ }
254
+ if (tag === "020") {
255
+ return subfields.filter((sub) => ["a", "z"].includes(sub.code) && testStringOrNumber(sub.value) && issnIsbnReqExp.test(String(sub.value))).map(({ value }) => String(value));
256
+ }
257
+ return subfields.filter((sub) => ["a", "z"].includes(sub.code) && testStringOrNumber(sub.value) && otherIdReqExp.test(String(sub.value))).map(({ value }) => String(value));
258
+ }
259
+ }
260
+ //# sourceMappingURL=bib.js.map