From 287c1e235acefc39afa6b8c297b42eca01085361 Mon Sep 17 00:00:00 2001 From: danamansana Date: Mon, 13 Apr 2026 17:00:47 -0400 Subject: [PATCH 1/7] Add mapping of controlled vocab fields --- .../cql/controlledVocabularies.json | 2974 +++++++++++++++++ lib/elasticsearch/cql/index-mapping.js | 1 + lib/elasticsearch/cql_query_builder.js | 47 +- test/cql_query_builder.test.js | 34 +- test/fixtures/cql_fixtures.js | 432 ++- 5 files changed, 3467 insertions(+), 21 deletions(-) create mode 100644 lib/elasticsearch/cql/controlledVocabularies.json diff --git a/lib/elasticsearch/cql/controlledVocabularies.json b/lib/elasticsearch/cql/controlledVocabularies.json new file mode 100644 index 00000000..749eea23 --- /dev/null +++ b/lib/elasticsearch/cql/controlledVocabularies.json @@ -0,0 +1,2974 @@ +{ + "format": [ + { + "value": "3", + "label": "E-video" + }, + { + "value": "4", + "label": "Tablet" + }, + { + "value": "5", + "label": "Read-along book" + }, + { + "value": "7", + "label": "Teacher audio" + }, + { + "value": "-", + "label": "Miscellaneous" + }, + { + "value": "a", + "label": "Book/text" + }, + { + "value": "b", + "label": "Blu-ray" + }, + { + "value": "c", + "label": "Notated music" + }, + { + "value": "d", + "label": "Manuscript notated music" + }, + { + "value": "e", + "label": "Map" + }, + { + "value": "g", + "label": "Film, slide, etc." + }, + { + "value": "h", + "label": "Microform" + }, + { + "value": "i", + "label": "Spoken word recording" + }, + { + "value": "j", + "label": "Musical recording" + }, + { + "value": "k", + "label": "Picture" + }, + { + "value": "l", + "label": "Large print" + }, + { + "value": "m", + "label": "Computer file" + }, + { + "value": "n", + "label": "E-audiobook" + }, + { + "value": "o", + "label": "Kit" + }, + { + "value": "p", + "label": "Archival mix" + }, + { + "value": "r", + "label": "3-D object" + }, + { + "value": "s", + "label": "VHS" + }, + { + "value": "t", + "label": "Manuscript" + }, + { + "value": "u", + "label": "Audiobook" + }, + { + "value": "v", + "label": "DVD" + }, + { + "value": "w", + "label": "Web resource" + }, + { + "value": "y", + "label": "Music CD" + }, + { + "value": "z", + "label": "E-book" + } + ], + "division": [ + { + "value": "bur", + "label": "Yoseloff Business Center", + "holdingLocations": [ + "bur51" + ] + }, + { + "value": "mab", + "label": "Art & Architecture Collection", + "holdingLocations": [ + "mab01", + "mab0v", + "mab82", + "mab88", + "mab89", + "mab92", + "mab98", + "mab99", + "mabb1", + "mabb2", + "mabb3", + "mabm2", + "rcmb2", + "rcmb8", + "rcmb9" + ] + }, + { + "value": "mac", + "label": "Arents Collection", + "holdingLocations": [ + "mac82", + "macc2" + ] + }, + { + "value": "mae", + "label": "Berg Collection", + "holdingLocations": [ + "mae82", + "mae92", + "maee2" + ] + }, + { + "value": "maf", + "label": "Dorot Jewish Division", + "holdingLocations": [ + "maf32", + "maf82", + "maf88", + "maf92", + "maf98", + "maf99", + "maff1", + "maff3", + "rcmf2", + "rcmf8", + "rcmf9" + ] + }, + { + "value": "mag", + "label": "Milstein Division", + "holdingLocations": [ + "mag82", + "mag92", + "mag98", + "magg1", + "magg2", + "rcmg2", + "rcmg8", + "rcmg9", + "magg3" + ] + }, + { + "value": "mak", + "label": "DeWitt Wallace Periodical Room", + "holdingLocations": [ + "mak32", + "mak82", + "makk3" + ] + }, + { + "value": "mal", + "label": "General Research Division", + "holdingLocations": [ + "mai32", + "mai82", + "mai92", + "mal17", + "mal23", + "mal42", + "mal72", + "mal82", + "mal92", + "mala", + "mala1", + "malc", + "mall1", + "malm2", + "maln", + "maln1", + "malv2", + "malw", + "malw1", + "rc2ma", + "rc2mj", + "rcma2", + "rcmi2", + "rcmj2", + "rcml2", + "rcml8" + ] + }, + { + "value": "mao", + "label": "Manuscripts and Archives Division", + "holdingLocations": [ + "mao82", + "mao92", + "maor2", + "rcmo2" + ] + }, + { + "value": "map", + "label": "Lionel Pincus and Princess Firyal Map Division", + "holdingLocations": [ + "map08", + "map32", + "map42", + "map82", + "map92", + "map98", + "map99", + "mapp1", + "mapp2", + "mapp3", + "mapp8", + "mapp9", + "rcmp2", + "rcmp8", + "rcmp9" + ] + }, + { + "value": "maq", + "label": "Pforzheimer Collection", + "holdingLocations": [ + "maqq2", + "rcmq2" + ] + }, + { + "value": "mar", + "label": "Rare Book Division", + "holdingLocations": [ + "mar82", + "mar92", + "mard2", + "marr2", + "rcmr2" + ] + }, + { + "value": "mas", + "label": "Photography Collection", + "holdingLocations": [ + "mas82", + "mas92", + "masb2", + "mass2", + "masu2", + "rcms2" + ] + }, + { + "value": "mau", + "label": "Print Collection", + "holdingLocations": [ + "mauu1", + "mauu2" + ] + }, + { + "value": "pad", + "label": "Dance Division", + "holdingLocations": [ + "pad11", + "pad22", + "pad28", + "pad32", + "pad38", + "pad42", + "rcpd2", + "rcpd8", + "rcpd9" + ] + }, + { + "value": "paf", + "label": "Theatre on Film and Tape Archive", + "holdingLocations": [ + "paf28", + "paf29", + "rcpf2", + "rcpf8", + "rcpf9" + ] + }, + { + "value": "pah", + "label": "Archives of Recorded Sound", + "holdingLocations": [ + "pah11", + "pah22", + "pah32", + "pah38", + "pah42", + "rcph2", + "rcph8", + "rcph9" + ] + }, + { + "value": "pam", + "label": "Music Division", + "holdingLocations": [ + "pam11", + "pam22", + "pam32", + "pam38", + "pam42", + "pam48", + "rcpm2", + "rcpm8", + "rcpm9" + ] + }, + { + "value": "pat", + "label": "Theatre Division", + "holdingLocations": [ + "pat11", + "pat22", + "pat28", + "pat32", + "pat38", + "pat42", + "pat48", + "rcpt2", + "rcpt8", + "rcpt9" + ] + }, + { + "value": "pav", + "label": "Reserve Film and Video", + "holdingLocations": [ + "pav22", + "pav28", + "pav32", + "pav38", + "rcpr2", + "rcpr8", + "rcpr9" + ] + }, + { + "value": "scb", + "label": "Moving Image and Recorded Sound Division", + "holdingLocations": [ + "scbb2", + "rccb2", + "rccb8", + "rccb9" + ] + }, + { + "value": "scc", + "label": "Art and Artifacts Division", + "holdingLocations": [ + "sccc2", + "sccc3", + "rcca9" + ] + }, + { + "value": "scd", + "label": "Manuscripts, Archives and Rare Books Division", + "holdingLocations": [ + "scdd1", + "scdd2", + "rccd2", + "rccd8", + "rccd9" + ] + }, + { + "value": "sce", + "label": "Photographs and Prints Division", + "holdingLocations": [ + "scedd", + "scee2", + "rcce2", + "rcce8", + "rcce9" + ] + }, + { + "value": "scf", + "label": "Jean Blackwell Hutson Research and Reference Division", + "holdingLocations": [ + "scff1", + "scff2", + "scff3", + "rc2cf", + "rc2cm", + "rccf2", + "rccf8", + "rccf9" + ] + } + ], + "language": [ + { + "value": "lang:eng", + "count": 8489777, + "label": "English" + }, + { + "value": "lang:ger", + "count": 1907347, + "label": "German" + }, + { + "value": "lang:fre", + "count": 1511883, + "label": "French" + }, + { + "value": "lang:spa", + "count": 1284404, + "label": "Spanish" + }, + { + "value": "lang:ita", + "count": 859352, + "label": "Italian" + }, + { + "value": "lang:rus", + "count": 786850, + "label": "Russian" + }, + { + "value": "lang:ara", + "count": 511491, + "label": "Arabic" + }, + { + "value": "lang:chi", + "count": 485572, + "label": "Chinese" + }, + { + "value": "lang:heb", + "count": 300202, + "label": "Hebrew" + }, + { + "value": "lang:jpn", + "count": 297317, + "label": "Japanese" + }, + { + "value": "lang:por", + "count": 296936, + "label": "Portuguese" + }, + { + "value": "lang:zxx", + "count": 282185, + "label": "No linguistic content" + }, + { + "value": "lang:pol", + "count": 219661, + "label": "Polish" + }, + { + "value": "lang:dut", + "count": 182321, + "label": "Dutch" + }, + { + "value": "lang:tur", + "count": 155638, + "label": "Turkish" + }, + { + "value": "lang:kor", + "count": 145186, + "label": "Korean" + }, + { + "value": "lang:per", + "count": 122335, + "label": "Persian" + }, + { + "value": "lang:lat", + "count": 101220, + "label": "Latin" + }, + { + "value": "lang:ukr", + "count": 96752, + "label": "Ukrainian" + }, + { + "value": "lang:swe", + "count": 95903, + "label": "Swedish" + }, + { + "value": "lang:gre", + "count": 85529, + "label": "Greek, Modern (1453- )" + }, + { + "value": "lang:cze", + "count": 82038, + "label": "Czech" + }, + { + "value": "lang:", + "count": 74655, + "label": "" + }, + { + "value": "lang:dan", + "count": 64515, + "label": "Danish" + }, + { + "value": "lang:hun", + "count": 58734, + "label": "Hungarian" + }, + { + "value": "lang:hin", + "count": 48446, + "label": "Hindi" + }, + { + "value": "lang:bul", + "count": 47754, + "label": "Bulgarian" + }, + { + "value": "lang:cat", + "count": 43354, + "label": "Catalan" + }, + { + "value": "lang:urd", + "count": 42413, + "label": "Urdu" + }, + { + "value": "lang:rum", + "count": 39776, + "label": "Romanian" + }, + { + "value": "lang:nor", + "count": 39526, + "label": "Norwegian" + }, + { + "value": "lang:fin", + "count": 36866, + "label": "Finnish" + }, + { + "value": "lang:hrv", + "count": 36442, + "label": "Croatian" + }, + { + "value": "lang:srp", + "count": 33200, + "label": "Serbian" + }, + { + "value": "lang:ben", + "count": 27098, + "label": "Bengali" + }, + { + "value": "lang:yid", + "count": 24854, + "label": "Yiddish" + }, + { + "value": "lang:tam", + "count": 24722, + "label": "Tamil" + }, + { + "value": "lang:und", + "count": 24304, + "label": "Undetermined" + }, + { + "value": "lang:arm", + "count": 24209, + "label": "Armenian" + }, + { + "value": "lang:mul", + "count": 23185, + "label": "Multiple languages" + }, + { + "value": "lang:slo", + "count": 19316, + "label": "Slovak" + }, + { + "value": "lang:lav", + "count": 18323, + "label": "Latvian" + }, + { + "value": "lang:N/A", + "count": 16165, + "label": "" + }, + { + "value": "lang:san", + "count": 16164, + "label": "Sanskrit" + }, + { + "value": "lang:tib", + "count": 14832, + "label": "Tibetan" + }, + { + "value": "lang:ind", + "count": 14443, + "label": "Indonesian" + }, + { + "value": "lang:bel", + "count": 14384, + "label": "Belarusian" + }, + { + "value": "lang:ota", + "count": 11470, + "label": "Turkish, Ottoman" + }, + { + "value": "lang:slv", + "count": 11380, + "label": "Slovenian" + }, + { + "value": "lang:lit", + "count": 11171, + "label": "Lithuanian" + }, + { + "value": "lang:geo", + "count": 10989, + "label": "Georgian" + }, + { + "value": "lang:est", + "count": 10199, + "label": "Estonian" + }, + { + "value": "lang:aze", + "count": 9695, + "label": "Azerbaijani" + }, + { + "value": "lang:uzb", + "count": 9429, + "label": "Uzbek" + }, + { + "value": "lang:mac", + "count": 9116, + "label": "Macedonian" + }, + { + "value": "lang:ice", + "count": 8698, + "label": "Icelandic" + }, + { + "value": "lang:pan", + "count": 8400, + "label": "Panjabi" + }, + { + "value": "lang:afr", + "count": 8373, + "label": "Afrikaans" + }, + { + "value": "lang:alb", + "count": 8363, + "label": "Albanian" + }, + { + "value": "lang:kaz", + "count": 7652, + "label": "Kazakh" + }, + { + "value": "lang:nep", + "count": 6738, + "label": "Nepali" + }, + { + "value": "lang:kur", + "count": 6518, + "label": "Kurdish" + }, + { + "value": "lang:grc", + "count": 5807, + "label": "Greek, Ancient (to 1453)" + }, + { + "value": "lang:tel", + "count": 5761, + "label": "Telugu" + }, + { + "value": "lang:glg", + "count": 5733, + "label": "Galician" + }, + { + "value": "lang:wel", + "count": 5731, + "label": "Welsh" + }, + { + "value": "lang:mal", + "count": 5660, + "label": "Malayalam" + }, + { + "value": "lang:kan", + "count": 5310, + "label": "Kannada" + }, + { + "value": "lang:gle", + "count": 5121, + "label": "Irish" + }, + { + "value": "lang:bos", + "count": 4992, + "label": "Bosnian" + }, + { + "value": "lang:snd", + "count": 4847, + "label": "Sindhi" + }, + { + "value": "lang:vie", + "count": 4825, + "label": "Vietnamese" + }, + { + "value": "lang:kir", + "count": 4289, + "label": "Kyrgyz" + }, + { + "value": "lang:scr", + "count": 4084, + "label": "" + }, + { + "value": "lang:swa", + "count": 3992, + "label": "Swahili" + }, + { + "value": "lang:ori", + "count": 3604, + "label": "Oriya" + }, + { + "value": "lang:pus", + "count": 3565, + "label": "Pushto" + }, + { + "value": "lang:tat", + "count": 3334, + "label": "Tatar" + }, + { + "value": "lang:mon", + "count": 3121, + "label": "Mongolian" + }, + { + "value": "lang:guj", + "count": 2837, + "label": "Gujarati" + }, + { + "value": "lang:may", + "count": 2787, + "label": "Malay" + }, + { + "value": "lang:tgk", + "count": 2651, + "label": "Tajik" + }, + { + "value": "lang:uig", + "count": 2517, + "label": "Uighur" + }, + { + "value": "lang:amh", + "count": 2452, + "label": "Amharic" + }, + { + "value": "lang:scc", + "count": 2441, + "label": "" + }, + { + "value": "lang:bak", + "count": 2426, + "label": "Bashkir" + }, + { + "value": "lang:mar", + "count": 2403, + "label": "Marathi" + }, + { + "value": "lang:asm", + "count": 2316, + "label": "Assamese" + }, + { + "value": "lang:epo", + "count": 2226, + "label": "Esperanto" + }, + { + "value": "lang:tuk", + "count": 2136, + "label": "Turkmen" + }, + { + "value": "lang:tut", + "count": 2041, + "label": "Altaic (Other)" + }, + { + "value": "lang:chv", + "count": 2004, + "label": "Chuvash" + }, + { + "value": "lang:tha", + "count": 1804, + "label": "Thai" + }, + { + "value": "lang:baq", + "count": 1756, + "label": "Basque" + }, + { + "value": "lang:sin", + "count": 1449, + "label": "Sinhalese" + }, + { + "value": "lang:nob", + "count": 1377, + "label": "Norwegian (Bokmål)" + }, + { + "value": "lang:fiu", + "count": 1255, + "label": "Finno-Ugrian (Other)" + }, + { + "value": "lang:yor", + "count": 1227, + "label": "Yoruba" + }, + { + "value": "lang:chu", + "count": 1216, + "label": "Church Slavic" + }, + { + "value": "lang:cau", + "count": 1213, + "label": "Caucasian (Other)" + }, + { + "value": "lang:tjk", + "count": 1199, + "label": "" + }, + { + "value": "lang:oci", + "count": 1094, + "label": "Occitan (post-1500)" + }, + { + "value": "lang:nic", + "count": 1073, + "label": "Niger-Kordofanian (Other)" + }, + { + "value": "lang:fry", + "count": 1054, + "label": "Frisian" + }, + { + "value": "lang:raj", + "count": 1049, + "label": "Rajasthani" + }, + { + "value": "lang:bur", + "count": 1025, + "label": "Burmese" + }, + { + "value": "lang:myn", + "count": 1015, + "label": "Mayan languages" + }, + { + "value": "lang:mlt", + "count": 992, + "label": "Maltese" + }, + { + "value": "lang:wen", + "count": 945, + "label": "Sorbian (Other)" + }, + { + "value": "lang:bre", + "count": 864, + "label": "Breton" + }, + { + "value": "lang:fro", + "count": 849, + "label": "French, Old (ca. 842-1300)" + }, + { + "value": "lang:new", + "count": 824, + "label": "Newari" + }, + { + "value": "lang:roa", + "count": 821, + "label": "Romance (Other)" + }, + { + "value": "lang:mlg", + "count": 787, + "label": "Malagasy" + }, + { + "value": "lang:cpf", + "count": 771, + "label": "Creoles and Pidgins, French-based (Other)" + }, + { + "value": "lang:pra", + "count": 743, + "label": "Prakrit languages" + }, + { + "value": "lang:sna", + "count": 722, + "label": "Shona" + }, + { + "value": "lang:gem", + "count": 705, + "label": "Germanic (Other)" + }, + { + "value": "lang:roh", + "count": 694, + "label": "Raeto-Romance" + }, + { + "value": "lang:fao", + "count": 679, + "label": "Faroese" + }, + { + "value": "lang:gmh", + "count": 670, + "label": "German, Middle High (ca. 1050-1500)" + }, + { + "value": "lang:frm", + "count": 655, + "label": "French, Middle (ca. 1300-1600)" + }, + { + "value": "lang:bal", + "count": 654, + "label": "Baluchi" + }, + { + "value": "lang:hau", + "count": 642, + "label": "Hausa" + }, + { + "value": "lang:map", + "count": 621, + "label": "Austronesian (Other)" + }, + { + "value": "lang:lad", + "count": 611, + "label": "Ladino" + }, + { + "value": "lang:enm", + "count": 598, + "label": "English, Middle (1100-1500)" + }, + { + "value": "lang:pli", + "count": 583, + "label": "Pali" + }, + { + "value": "lang:gla", + "count": 573, + "label": "Scottish Gaelic" + }, + { + "value": "lang:som", + "count": 565, + "label": "Somali" + }, + { + "value": "lang:syr", + "count": 563, + "label": "Syriac, Modern" + }, + { + "value": "lang:bnt", + "count": 556, + "label": "Bantu (Other)" + }, + { + "value": "lang:oss", + "count": 550, + "label": "Ossetic" + }, + { + "value": "lang:pro", + "count": 532, + "label": "Provençal (to 1500)" + }, + { + "value": "lang:mai", + "count": 512, + "label": "Maithili" + }, + { + "value": "lang:inc", + "count": 501, + "label": "Indic (Other)" + }, + { + "value": "lang:ber", + "count": 500, + "label": "Berber (Other)" + }, + { + "value": "lang:chm", + "count": 500, + "label": "Mari" + }, + { + "value": "lang:sai", + "count": 499, + "label": "South American Indian (Other)" + }, + { + "value": "lang:tir", + "count": 457, + "label": "Tigrinya" + }, + { + "value": "lang:rom", + "count": 447, + "label": "Romani" + }, + { + "value": "lang:egy", + "count": 445, + "label": "Egyptian" + }, + { + "value": "lang:sot", + "count": 443, + "label": "Sotho" + }, + { + "value": "lang:sah", + "count": 427, + "label": "Yakut" + }, + { + "value": "lang:hat", + "count": 402, + "label": "Haitian French Creole" + }, + { + "value": "lang:dra", + "count": 401, + "label": "Dravidian (Other)" + }, + { + "value": "lang:paa", + "count": 397, + "label": "Papuan (Other)" + }, + { + "value": "lang:tgl", + "count": 391, + "label": "Tagalog" + }, + { + "value": "lang:udm", + "count": 389, + "label": "Udmurt" + }, + { + "value": "lang:lug", + "count": 387, + "label": "Ganda" + }, + { + "value": "lang:lus", + "count": 376, + "label": "Lushai" + }, + { + "value": "lang:xho", + "count": 372, + "label": "Xhosa" + }, + { + "value": "lang:tsn", + "count": 368, + "label": "Tswana" + }, + { + "value": "lang:zul", + "count": 359, + "label": "Zulu" + }, + { + "value": "lang:kin", + "count": 346, + "label": "Kinyarwanda" + }, + { + "value": "lang:ful", + "count": 324, + "label": "Fula" + }, + { + "value": "lang:kaa", + "count": 319, + "label": "Kara-Kalpak" + }, + { + "value": "lang:sla", + "count": 306, + "label": "Slavic (Other)" + }, + { + "value": "lang:nya", + "count": 303, + "label": "Nyanja" + }, + { + "value": "lang:kom", + "count": 302, + "label": "Komi" + }, + { + "value": "lang:wol", + "count": 299, + "label": "Wolof" + }, + { + "value": "lang:nde", + "count": 298, + "label": "Ndebele (Zimbabwe)" + }, + { + "value": "lang:jrb", + "count": 293, + "label": "Judeo-Arabic" + }, + { + "value": "lang:orm", + "count": 291, + "label": "Oromo" + }, + { + "value": "lang:che", + "count": 285, + "label": "Chechen" + }, + { + "value": "lang:mol", + "count": 285, + "label": "" + }, + { + "value": "lang:bho", + "count": 284, + "label": "Bhojpuri" + }, + { + "value": "lang:ang", + "count": 281, + "label": "English, Old (ca. 450-1100)" + }, + { + "value": "lang:bra", + "count": 281, + "label": "Braj" + }, + { + "value": "lang:que", + "count": 266, + "label": "Quechua" + }, + { + "value": "lang:cai", + "count": 262, + "label": "Central American Indian (Other)" + }, + { + "value": "lang:sit", + "count": 253, + "label": "Sino-Tibetan (Other)" + }, + { + "value": "lang:crp", + "count": 244, + "label": "Creoles and Pidgins (Other)" + }, + { + "value": "lang:grn", + "count": 233, + "label": "Guarani" + }, + { + "value": "lang:jav", + "count": 230, + "label": "Javanese" + }, + { + "value": "lang:twi", + "count": 219, + "label": "Twi" + }, + { + "value": "lang:nno", + "count": 216, + "label": "Norwegian (Nynorsk)" + }, + { + "value": "lang:lah", + "count": 214, + "label": "Lahndā" + }, + { + "value": "lang:ibo", + "count": 213, + "label": "Igbo" + }, + { + "value": "lang:doi", + "count": 209, + "label": "Dogri" + }, + { + "value": "lang:arc", + "count": 207, + "label": "Aramaic" + }, + { + "value": "lang:kas", + "count": 203, + "label": "Kashmiri" + }, + { + "value": "lang:haw", + "count": 197, + "label": "Hawaiian" + }, + { + "value": "lang:man", + "count": 189, + "label": "Mandingo" + }, + { + "value": "lang:kha", + "count": 186, + "label": "Khasi" + }, + { + "value": "lang:ssa", + "count": 173, + "label": "Nilo-Saharan (Other)" + }, + { + "value": "lang:phi", + "count": 161, + "label": "Philippine (Other)" + }, + { + "value": "lang:mis", + "count": 157, + "label": "Miscellaneous languages" + }, + { + "value": "lang:bam", + "count": 155, + "label": "Bambara" + }, + { + "value": "lang:cop", + "count": 155, + "label": "Coptic" + }, + { + "value": "lang:afa", + "count": 154, + "label": "Afroasiatic (Other)" + }, + { + "value": "lang:ava", + "count": 154, + "label": "Avaric" + }, + { + "value": "lang:nai", + "count": 153, + "label": "North American Indian (Other)" + }, + { + "value": "lang:nah", + "count": 148, + "label": "Nahuatl" + }, + { + "value": "lang:nso", + "count": 141, + "label": "Northern Sotho" + }, + { + "value": "lang:kal", + "count": 137, + "label": "Kalâtdlisut" + }, + { + "value": "lang:kbd", + "count": 133, + "label": "Kabardian" + }, + { + "value": "lang:ewe", + "count": 132, + "label": "Ewe" + }, + { + "value": "lang:pap", + "count": 132, + "label": "Papiamento" + }, + { + "value": "lang:ady", + "count": 129, + "label": "Adygei" + }, + { + "value": "lang:cre", + "count": 129, + "label": "Cree" + }, + { + "value": "lang:sco", + "count": 125, + "label": "Scots" + }, + { + "value": "lang:kon", + "count": 124, + "label": "Kongo" + }, + { + "value": "lang:oji", + "count": 118, + "label": "Ojibwa" + }, + { + "value": "lang:N A", + "count": 116, + "label": "" + }, + { + "value": "lang:khm", + "count": 116, + "label": "Khmer" + }, + { + "value": "lang:mao", + "count": 116, + "label": "Maori" + }, + { + "value": "lang:gez", + "count": 112, + "label": "Ethiopic" + }, + { + "value": "lang:ndo", + "count": 111, + "label": "Ndonga" + }, + { + "value": "lang:cpp", + "count": 107, + "label": "Creoles and Pidgins, Portuguese-based (Other)" + }, + { + "value": "lang:dzo", + "count": 107, + "label": "Dzongkha" + }, + { + "value": "lang:kab", + "count": 107, + "label": "Kabyle" + }, + { + "value": "lang:ira", + "count": 106, + "label": "Iranian (Other)" + }, + { + "value": "lang:awa", + "count": 102, + "label": "Awadhi" + }, + { + "value": "lang:gaa", + "count": 101, + "label": "Gã" + }, + { + "value": "lang:ssw", + "count": 99, + "label": "Swazi" + }, + { + "value": "lang:chg", + "count": 98, + "label": "Chagatai" + }, + { + "value": "lang:mus", + "count": 95, + "label": "Creek" + }, + { + "value": "lang:smi", + "count": 95, + "label": "Sami" + }, + { + "value": "lang:tso", + "count": 95, + "label": "Tsonga" + }, + { + "value": "lang:akk", + "count": 94, + "label": "Akkadian" + }, + { + "value": "lang:bua", + "count": 94, + "label": "Buriat" + }, + { + "value": "lang:dak", + "count": 93, + "label": "Dakota" + }, + { + "value": "lang:kik", + "count": 93, + "label": "Kikuyu" + }, + { + "value": "lang:mno", + "count": 92, + "label": "Manobo languages" + }, + { + "value": "lang:bem", + "count": 91, + "label": "Bemba" + }, + { + "value": "lang:cpe", + "count": 90, + "label": "Creoles and Pidgins, English-based (Other)" + }, + { + "value": "lang:ypk", + "count": 89, + "label": "Yupik languages" + }, + { + "value": "lang:cho", + "count": 88, + "label": "Choctaw" + }, + { + "value": "lang:dum", + "count": 87, + "label": "Dutch, Middle (ca. 1050-1350)" + }, + { + "value": "lang:zza", + "count": 87, + "label": "Zaza" + }, + { + "value": "lang:mni", + "count": 86, + "label": "Manipuri" + }, + { + "value": "lang:gag", + "count": 85, + "label": "" + }, + { + "value": "lang:lao", + "count": 85, + "label": "Lao" + }, + { + "value": "lang:mos", + "count": 82, + "label": "Mooré" + }, + { + "value": "lang:srd", + "count": 82, + "label": "Sardinian" + }, + { + "value": "lang:myv", + "count": 81, + "label": "Erzya" + }, + { + "value": "lang:sun", + "count": 80, + "label": "Sundanese" + }, + { + "value": "lang:ast", + "count": 79, + "label": "Bable" + }, + { + "value": "lang:pal", + "count": 79, + "label": "Pahlavi" + }, + { + "value": "lang:aym", + "count": 78, + "label": "Aymara" + }, + { + "value": "lang:zap", + "count": 75, + "label": "Zapotec" + }, + { + "value": "lang:nds", + "count": 72, + "label": "Low German" + }, + { + "value": "lang:snk", + "count": 72, + "label": "Soninke" + }, + { + "value": "lang:alg", + "count": 69, + "label": "Algonquian (Other)" + }, + { + "value": "lang:run", + "count": 69, + "label": "Rundi" + }, + { + "value": "lang:got", + "count": 67, + "label": "Gothic" + }, + { + "value": "lang:abk", + "count": 66, + "label": "Abkhaz" + }, + { + "value": "lang:kua", + "count": 66, + "label": "Kuanyama" + }, + { + "value": "lang:chr", + "count": 65, + "label": "Cherokee" + }, + { + "value": "lang:fur", + "count": 65, + "label": "Friulian" + }, + { + "value": "lang:xal", + "count": 64, + "label": "Oirat" + }, + { + "value": "lang:mdf", + "count": 63, + "label": "Moksha" + }, + { + "value": "lang:srr", + "count": 63, + "label": "Serer" + }, + { + "value": "lang:him", + "count": 62, + "label": "Western Pahari languages" + }, + { + "value": "lang:iri", + "count": 62, + "label": "" + }, + { + "value": "lang:kum", + "count": 59, + "label": "Kumyk" + }, + { + "value": "lang:crh", + "count": 57, + "label": "Crimean Tatar" + }, + { + "value": "lang:son", + "count": 57, + "label": "Songhai" + }, + { + "value": "lang:khi", + "count": 56, + "label": "Khoisan (Other)" + }, + { + "value": "lang:ban", + "count": 55, + "label": "Balinese" + }, + { + "value": "lang:cos", + "count": 55, + "label": "Corsican" + }, + { + "value": "lang:loz", + "count": 55, + "label": "Lozi" + }, + { + "value": "lang:inh", + "count": 54, + "label": "Ingush" + }, + { + "value": "lang:lin", + "count": 54, + "label": "Lingala" + }, + { + "value": "lang:goh", + "count": 52, + "label": "German, Old High (ca. 750-1050)" + }, + { + "value": "lang:nap", + "count": 52, + "label": "Neapolitan Italian" + }, + { + "value": "lang:ven", + "count": 52, + "label": "Venda" + }, + { + "value": "lang:fat", + "count": 51, + "label": "Fanti" + }, + { + "value": "lang:her", + "count": 51, + "label": "Herero" + }, + { + "value": "lang:kar", + "count": 50, + "label": "Karen languages" + }, + { + "value": "lang:moh", + "count": 50, + "label": "Mohawk" + }, + { + "value": "lang:mwr", + "count": 50, + "label": "Marwari" + }, + { + "value": "lang:sam", + "count": 50, + "label": "Samaritan Aramaic" + }, + { + "value": "lang:luo", + "count": 49, + "label": "Luo (Kenya and Tanzania)" + }, + { + "value": "lang:tyv", + "count": 49, + "label": "Tuvinian" + }, + { + "value": "lang:dyu", + "count": 48, + "label": "Dyula" + }, + { + "value": "lang:lan", + "count": 48, + "label": "" + }, + { + "value": "lang:nyn", + "count": 48, + "label": "Nyankole" + }, + { + "value": "lang:shn", + "count": 48, + "label": "Shan" + }, + { + "value": "lang:iku", + "count": 47, + "label": "Inuktitut" + }, + { + "value": "lang:taj", + "count": 46, + "label": "" + }, + { + "value": "lang:arn", + "count": 45, + "label": "Mapuche" + }, + { + "value": "lang:kro", + "count": 45, + "label": "Kru (Other)" + }, + { + "value": "lang:krc", + "count": 43, + "label": "Karachay-Balkar" + }, + { + "value": "lang:ltz", + "count": 42, + "label": "Luxembourgish" + }, + { + "value": "lang:krl", + "count": 41, + "label": "Karelian" + }, + { + "value": "lang:tar", + "count": 41, + "label": "" + }, + { + "value": "lang:cnr", + "count": 40, + "label": "" + }, + { + "value": "lang:nav", + "count": 40, + "label": "Navajo" + }, + { + "value": "lang:smo", + "count": 40, + "label": "Samoan" + }, + { + "value": "lang:sux", + "count": 40, + "label": "Sumerian" + }, + { + "value": "lang:tah", + "count": 40, + "label": "Tahitian" + }, + { + "value": "lang:efi", + "count": 39, + "label": "Efik" + }, + { + "value": "lang:gsw", + "count": 39, + "label": "Swiss German" + }, + { + "value": "lang:mnc", + "count": 39, + "label": "Manchu" + }, + { + "value": "lang:oto", + "count": 39, + "label": "Otomian languages" + }, + { + "value": "lang:gae", + "count": 38, + "label": "" + }, + { + "value": "lang:iro", + "count": 38, + "label": "Iroquoian (Other)" + }, + { + "value": "lang:aka", + "count": 37, + "label": "Akan" + }, + { + "value": "lang:art", + "count": 37, + "label": "Artificial (Other)" + }, + { + "value": "lang:lez", + "count": 36, + "label": "Lezgian" + }, + { + "value": "lang:non", + "count": 36, + "label": "Old Norse" + }, + { + "value": "lang:sho", + "count": 36, + "label": "" + }, + { + "value": "lang:cel", + "count": 35, + "label": "Celtic (Other)" + }, + { + "value": "lang:cor", + "count": 35, + "label": "Cornish" + }, + { + "value": "lang:men", + "count": 35, + "label": "Mende" + }, + { + "value": "lang:kok", + "count": 34, + "label": "Konkani" + }, + { + "value": "lang:sme", + "count": 34, + "label": "Northern Sami" + }, + { + "value": "lang:arg", + "count": 33, + "label": "Aragonese" + }, + { + "value": "lang:jpr", + "count": 33, + "label": "Judeo-Persian" + }, + { + "value": "lang:lol", + "count": 32, + "label": "Mongo-Nkundu" + }, + { + "value": "lang:aar", + "count": 31, + "label": "Afar" + }, + { + "value": "lang:ceb", + "count": 31, + "label": "Cebuano" + }, + { + "value": "lang:engger", + "count": 31, + "label": "" + }, + { + "value": "lang:kam", + "count": 31, + "label": "Kamba" + }, + { + "value": "lang:tmh", + "count": 31, + "label": "Tamashek" + }, + { + "value": "lang:bas", + "count": 30, + "label": "Basa" + }, + { + "value": "lang:ilo", + "count": 30, + "label": "Iloko" + }, + { + "value": "lang:ina", + "count": 30, + "label": "Interlingua (International Auxiliary Language Association)" + }, + { + "value": "lang:nzi", + "count": 30, + "label": "Nzima" + }, + { + "value": "lang:dar", + "count": 29, + "label": "Dargwa" + }, + { + "value": "lang:scn", + "count": 29, + "label": "Sicilian Italian" + }, + { + "value": "lang:ave", + "count": 27, + "label": "Avestan" + }, + { + "value": "lang:glv", + "count": 27, + "label": "Manx" + }, + { + "value": "lang:mun", + "count": 27, + "label": "Munda (Other)" + }, + { + "value": "lang:tog", + "count": 27, + "label": "Tonga (Nyasa)" + }, + { + "value": "lang:alt", + "count": 26, + "label": "Altai" + }, + { + "value": "lang:engfre", + "count": 26, + "label": "" + }, + { + "value": "lang:mag", + "count": 26, + "label": "Magahi" + }, + { + "value": "lang:tum", + "count": 26, + "label": "Tumbuka" + }, + { + "value": "lang:csb", + "count": 25, + "label": "Kashubian" + }, + { + "value": "lang:englat", + "count": 25, + "label": "" + }, + { + "value": "lang:fri", + "count": 25, + "label": "" + }, + { + "value": "lang:freeng", + "count": 24, + "label": "" + }, + { + "value": "lang:grb", + "count": 24, + "label": "Grebo" + }, + { + "value": "lang:ijo", + "count": 24, + "label": "Ijo" + }, + { + "value": "lang:tig", + "count": 24, + "label": "Tigré" + }, + { + "value": "lang:bih", + "count": 23, + "label": "Bihari (Other)" + }, + { + "value": "lang:spaeng", + "count": 23, + "label": "" + }, + { + "value": "lang:ada", + "count": 22, + "label": "Adangme" + }, + { + "value": "lang:ath", + "count": 22, + "label": "Athapascan (Other)" + }, + { + "value": "lang:esk", + "count": 22, + "label": "" + }, + { + "value": "lang:lateng", + "count": 22, + "label": "" + }, + { + "value": "lang:nyo", + "count": 22, + "label": "Nyoro" + }, + { + "value": "lang:fan", + "count": 21, + "label": "Fang" + }, + { + "value": "lang:fon", + "count": 21, + "label": "Fon" + }, + { + "value": "lang:lub", + "count": 20, + "label": "Luba-Katanga" + }, + { + "value": "lang:pon", + "count": 20, + "label": "Pohnpeian" + }, + { + "value": "lang:sal", + "count": 20, + "label": "Salishan languages" + }, + { + "value": "lang:esp", + "count": 19, + "label": "" + }, + { + "value": "lang:gereng", + "count": 19, + "label": "" + }, + { + "value": "lang:nub", + "count": 19, + "label": "Nubian languages" + }, + { + "value": "lang:ach", + "count": 18, + "label": "Acoli" + }, + { + "value": "lang:hil", + "count": 18, + "label": "Hiligaynon" + }, + { + "value": "lang:ido", + "count": 18, + "label": "Ido" + }, + { + "value": "lang:tem", + "count": 18, + "label": "Temne" + }, + { + "value": "lang:uga", + "count": 18, + "label": "Ugaritic" + }, + { + "value": "lang:a d", + "count": 17, + "label": "" + }, + { + "value": "lang:dua", + "count": 17, + "label": "Duala" + }, + { + "value": "lang:kaw", + "count": 17, + "label": "Kawi" + }, + { + "value": "lang:kru", + "count": 17, + "label": "Kurukh" + }, + { + "value": "lang:pam", + "count": 17, + "label": "Pampanga" + }, + { + "value": "lang:syc", + "count": 17, + "label": "Syriac" + }, + { + "value": "lang:gil", + "count": 16, + "label": "Gilbertese" + }, + { + "value": "lang:pag", + "count": 16, + "label": "Pangasinan" + }, + { + "value": "lang:aus", + "count": 15, + "label": "Australian languages" + }, + { + "value": "lang:cus", + "count": 15, + "label": "Cushitic (Other)" + }, + { + "value": "lang:den", + "count": 15, + "label": "Slavey" + }, + { + "value": "lang:eka", + "count": 15, + "label": "Ekajuk" + }, + { + "value": "lang:nbl", + "count": 15, + "label": "Ndebele (South Africa)" + }, + { + "value": "lang:wln", + "count": 15, + "label": "Walloon" + }, + { + "value": "lang:arw", + "count": 14, + "label": "Arawak" + }, + { + "value": "lang:bai", + "count": 14, + "label": "Bamileke languages" + }, + { + "value": "lang:del", + "count": 14, + "label": "Delaware" + }, + { + "value": "lang:ewo", + "count": 14, + "label": "Ewondo" + }, + { + "value": "lang:fij", + "count": 14, + "label": "Fijian" + }, + { + "value": "lang:nog", + "count": 14, + "label": "Nogai" + }, + { + "value": "lang:tai", + "count": 14, + "label": "Tai (Other)" + }, + { + "value": "lang:ton", + "count": 14, + "label": "Tongan" + }, + { + "value": "lang:apa", + "count": 13, + "label": "Apache languages" + }, + { + "value": "lang:din", + "count": 13, + "label": "Dinka" + }, + { + "value": "lang:eth", + "count": 13, + "label": "" + }, + { + "value": "lang:iba", + "count": 13, + "label": "Iban" + }, + { + "value": "lang:min", + "count": 13, + "label": "Minangkabau" + }, + { + "value": "lang:btk", + "count": 12, + "label": "Batak" + }, + { + "value": "lang:ine", + "count": 12, + "label": "Indo-European (Other)" + }, + { + "value": "lang:jap", + "count": 12, + "label": "" + }, + { + "value": "lang:mkh", + "count": 12, + "label": "Mon-Khmer (Other)" + }, + { + "value": "lang:pau", + "count": 12, + "label": "Palauan" + }, + { + "value": "lang:pa", + "count": 12, + "label": "|" + }, + { + "value": "lang:yao", + "count": 12, + "label": "Yao (Africa)" + }, + { + "value": "lang:engfreger", + "count": 11, + "label": "" + }, + { + "value": "lang:mah", + "count": 11, + "label": "Marshallese" + }, + { + "value": "lang:mic", + "count": 11, + "label": "Micmac" + }, + { + "value": "lang:sem", + "count": 11, + "label": "Semitic (Other)" + }, + { + "value": "lang:bat", + "count": 10, + "label": "Baltic (Other)" + }, + { + "value": "lang:car", + "count": 10, + "label": "Carib" + }, + { + "value": "lang:chn", + "count": 10, + "label": "Chinook jargon" + }, + { + "value": "lang:chy", + "count": 10, + "label": "Cheyenne" + }, + { + "value": "lang:mas", + "count": 10, + "label": "Maasai" + }, + { + "value": "lang:sio", + "count": 10, + "label": "Siouan (Other)" + }, + { + "value": "lang:tag", + "count": 10, + "label": "" + }, + { + "value": "lang:vol", + "count": 10, + "label": "Volapük" + }, + { + "value": "lang:bla", + "count": 9, + "label": "Siksika" + }, + { + "value": "lang:engita", + "count": 9, + "label": "" + }, + { + "value": "lang:engspa", + "count": 9, + "label": "" + }, + { + "value": "lang:ile", + "count": 9, + "label": "Interlingue" + }, + { + "value": "lang:kos", + "count": 9, + "label": "Kosraean" + }, + { + "value": "lang:lua", + "count": 9, + "label": "Luba-Lulua" + }, + { + "value": "lang:sad", + "count": 9, + "label": "Sandawe" + }, + { + "value": "lang:sag", + "count": 9, + "label": "Sango (Ubangi Creole)" + }, + { + "value": "lang:ace", + "count": 8, + "label": "Achinese" + }, + { + "value": "lang:bik", + "count": 8, + "label": "Bikol" + }, + { + "value": "lang:bug", + "count": 8, + "label": "Bugis" + }, + { + "value": "lang:chk", + "count": 8, + "label": "Chuukese" + }, + { + "value": "lang:far", + "count": 8, + "label": "" + }, + { + "value": "lang:fil", + "count": 8, + "label": "Filipino" + }, + { + "value": "lang:frr", + "count": 8, + "label": "North Frisian" + }, + { + "value": "lang:gal", + "count": 8, + "label": "" + }, + { + "value": "lang:itaeng", + "count": 8, + "label": "" + }, + { + "value": "lang:kmb", + "count": 8, + "label": "Kimbundu" + }, + { + "value": "lang:kpe", + "count": 8, + "label": "Kpelle" + }, + { + "value": "lang:mak", + "count": 8, + "label": "Makasar" + }, + { + "value": "lang:sat", + "count": 8, + "label": "Santali" + }, + { + "value": "lang:tet", + "count": 8, + "label": "Tetum" + }, + { + "value": "lang:tiv", + "count": 8, + "label": "Tiv" + }, + { + "value": "lang:vai", + "count": 8, + "label": "Vai" + }, + { + "value": "lang:wal", + "count": 8, + "label": "Wolayta" + }, + { + "value": "lang:zun", + "count": 8, + "label": "Zuni" + }, + { + "value": "lang:day", + "count": 7, + "label": "Dayak" + }, + { + "value": "lang:fra", + "count": 7, + "label": "" + }, + { + "value": "lang:gon", + "count": 7, + "label": "Gondi" + }, + { + "value": "lang:gua", + "count": 7, + "label": "" + }, + { + "value": "lang:kac", + "count": 7, + "label": "Kachin" + }, + { + "value": "lang:kau", + "count": 7, + "label": "Kanuri" + }, + { + "value": "lang:rup", + "count": 7, + "label": "Aromanian" + }, + { + "value": "lang:ale", + "count": 6, + "label": "Aleut" + }, + { + "value": "lang:bis", + "count": 6, + "label": "Bislama" + }, + { + "value": "lang:cha", + "count": 6, + "label": "Chamorro" + }, + { + "value": "lang:elx", + "count": 6, + "label": "Elamite" + }, + { + "value": "lang:gay", + "count": 6, + "label": "Gayo" + }, + { + "value": "lang:jpnchi", + "count": 6, + "label": "" + }, + { + "value": "lang:lun", + "count": 6, + "label": "Lunda" + }, + { + "value": "lang:sel", + "count": 6, + "label": "Selkup" + }, + { + "value": "lang:tli", + "count": 6, + "label": "Tlingit" + }, + { + "value": "lang:umb", + "count": 6, + "label": "Umbundu" + }, + { + "value": "lang:bin", + "count": 5, + "label": "Edo" + }, + { + "value": "lang:freengger", + "count": 5, + "label": "" + }, + { + "value": "lang:gerengfre", + "count": 5, + "label": "" + }, + { + "value": "lang:gerlat", + "count": 5, + "label": "" + }, + { + "value": "lang:hai", + "count": 5, + "label": "Haida" + }, + { + "value": "lang:lam", + "count": 5, + "label": "Lamba (Zambia and Congo)" + }, + { + "value": "lang:mad", + "count": 5, + "label": "Madurese" + }, + { + "value": "lang:mga", + "count": 5, + "label": "Irish, Middle (ca. 1100-1550)" + }, + { + "value": "lang:mwl", + "count": 5, + "label": "Mirandese" + }, + { + "value": "lang:sga", + "count": 5, + "label": "Irish, Old (to 1100)" + }, + { + "value": "lang:sus", + "count": 5, + "label": "Susu" + }, + { + "value": "lang:tsi", + "count": 5, + "label": "Tsimshian" + }, + { + "value": "lang:tsw", + "count": 5, + "label": "" + }, + { + "value": "lang:wak", + "count": 5, + "label": "Wakashan languages" + }, + { + "value": "lang:war", + "count": 5, + "label": "Waray" + }, + { + "value": "lang:0 s", + "count": 4, + "label": "" + }, + { + "value": "lang:]]]", + "count": 4, + "label": "" + }, + { + "value": "lang:_ru", + "count": 4, + "label": "" + }, + { + "value": "lang:anp", + "count": 4, + "label": "Angika" + }, + { + "value": "lang:bru", + "count": 4, + "label": "" + }, + { + "value": "lang:byn", + "count": 4, + "label": "Bilin" + }, + { + "value": "lang:enk", + "count": 4, + "label": "" + }, + { + "value": "lang:freger", + "count": 4, + "label": "" + }, + { + "value": "lang:gerfre", + "count": 4, + "label": "" + }, + { + "value": "lang:hmn", + "count": 4, + "label": "Hmong" + }, + { + "value": "lang:ipk", + "count": 4, + "label": "Inupiaq" + }, + { + "value": "lang:rar", + "count": 4, + "label": "Rarotongan" + }, + { + "value": "lang:rur", + "count": 4, + "label": "" + }, + { + "value": "lang:ruseng", + "count": 4, + "label": "" + }, + { + "value": "lang:sao", + "count": 4, + "label": "" + }, + { + "value": "lang:sid", + "count": 4, + "label": "Sidamo" + }, + { + "value": "lang:smj", + "count": 4, + "label": "Lule Sami" + }, + { + "value": "lang:snh", + "count": 4, + "label": "" + }, + { + "value": "lang:sog", + "count": 4, + "label": "Sogdian" + }, + { + "value": "lang:srn", + "count": 4, + "label": "Sranan" + }, + { + "value": "lang:ter", + "count": 4, + "label": "Terena" + }, + { + "value": "lang:tup", + "count": 4, + "label": "Tupi languages" + }, + { + "value": "lang:unr", + "count": 4, + "label": "" + }, + { + "value": "lang:vot", + "count": 4, + "label": "Votic" + }, + { + "value": "lang:|sp", + "count": 4, + "label": "" + }, + { + "value": "lang:cam", + "count": 3, + "label": "" + }, + { + "value": "lang:cmc", + "count": 3, + "label": "Chamic languages" + }, + { + "value": "lang:dsb", + "count": 3, + "label": "Lower Sorbian" + }, + { + "value": "lang:dutger", + "count": 3, + "label": "" + }, + { + "value": "lang:engfregerita", + "count": 3, + "label": "" + }, + { + "value": "lang:engiri", + "count": 3, + "label": "" + }, + { + "value": "lang:engjpn", + "count": 3, + "label": "" + }, + { + "value": "lang:engund", + "count": 3, + "label": "" + }, + { + "value": "lang:gba", + "count": 3, + "label": "Gbaya" + }, + { + "value": "lang:hit", + "count": 3, + "label": "Hittite" + }, + { + "value": "lang:hsb", + "count": 3, + "label": "Upper Sorbian" + }, + { + "value": "lang:int", + "count": 3, + "label": "" + }, + { + "value": "lang:jbo", + "count": 3, + "label": "Lojban (Artificial language)" + } + ], + "center": [ + { + "value": "ma", + "label": "Stephen A. Schwarzman Building (SASB)", + "nickname": "SASB" + }, + { + "value": "pa", + "label": "The New York Public Library for the Performing Arts (LPA)", + "nickname": "LPA" + }, + { + "value": "sc", + "label": "Schomburg Center for Research in Black Culture", + "nickname": "Schomburg" + }, + { + "value": "rc", + "label": "Offsite - request in advance" + }, + { + "value": "bu", + "label": "Stavros Niarchos Foundation Library (SNFL)", + "nickname": "SNFL" + } + ] +} + \ No newline at end of file diff --git a/lib/elasticsearch/cql/index-mapping.js b/lib/elasticsearch/cql/index-mapping.js index 0befbc87..ad84a70e 100644 --- a/lib/elasticsearch/cql/index-mapping.js +++ b/lib/elasticsearch/cql/index-mapping.js @@ -112,6 +112,7 @@ const indexMapping = { ] }, callnumber: { + fields: ['shelfMark'], term: ['shelfMark.keywordLowercased', 'items.shelfMark.keywordLowercased'] }, identifier: { diff --git a/lib/elasticsearch/cql_query_builder.js b/lib/elasticsearch/cql_query_builder.js index 58fb7a27..02fc6d34 100644 --- a/lib/elasticsearch/cql_query_builder.js +++ b/lib/elasticsearch/cql_query_builder.js @@ -2,6 +2,7 @@ const { parseWithRightCql, reverseString, parsedASTtoNestedArray } = require('./ const { indexMapping } = require('./cql/index-mapping') const ElasticQueryBuilder = require('./elastic-query-builder') const { InvalidParameterError } = require('../errors') +const controlledVocabularies = require('./cql/controlledVocabularies.json') class CqlQuery { constructor (queryStr) { @@ -203,19 +204,19 @@ function buildAtomic ({ scope, relation, terms, term }) { return { bool: { should: [ - buildAtomicMain({ fields: bibFields, relation, terms, term }), - (hasFields(itemFields) && buildAtomicNested('items', { fields: itemFields, relation, terms, term })), - (hasFields(holdingsFields) && buildAtomicNested('holdings', { fields: holdingsFields, relation, terms, term })) + buildAtomicMain({ fields: bibFields, relation, terms, term, scope }), + (hasFields(itemFields) && buildAtomicNested('items', { fields: itemFields, relation, terms, term, scope })), + (hasFields(holdingsFields) && buildAtomicNested('holdings', { fields: holdingsFields, relation, terms, term, scope })) ].filter(x => x) } } } -function buildAtomicNested (name, { fields, relation, terms, term }) { +function buildAtomicNested (name, { fields, relation, terms, term, scope }) { return { nested: { path: name, - query: buildAtomicMain({ fields, relation, terms, term }) + query: buildAtomicMain({ fields, relation, terms, term, scope }) } } } @@ -229,7 +230,7 @@ function buildAtomicNested (name, { fields, relation, terms, term }) { - put all terms in prefix match with prefix fields - put all terms in term matches with term fields */ -function buildAtomicMain ({ fields, relation, terms, term }) { +function buildAtomicQueryByRelation ({ fields, relation, terms, term }) { switch (relation) { case 'any': case 'all': @@ -250,6 +251,37 @@ function buildAtomicMain ({ fields, relation, terms, term }) { } } +function buildAtomicMain ({ fields, relation, terms, term, scope }) { + return !Object.keys(controlledVocabularies).includes(scope) + ? buildAtomicQueryByRelation({ fields, relation, terms, term }) + : mappedQueryForControlledVocabulary({ fields, relation, terms, term, scope }) +} + +function mappedQueryForControlledVocabulary ({ fields, relation, terms, term, scope }) { + // If relation is any/all, termsToMap should be terms, otherwise it should be [term] + const termsToMap = ['any', 'all'].includes(relation) ? terms : [term] + const controlledVocabFields = controlledVocabularies[scope] + const normalizer = str => str.toLowerCase().replace(/[^a-zA-Z0-9 ]/g, '') + const fieldMatcher = relation === '==' + ? (queryTerm) => field => field.value === queryTerm + : (queryTerm) => + field => [field.value, field.label].some(property => normalizer(property).includes(normalizer(queryTerm))) + + const mappedQueries = termsToMap.map((queryTerm) => { + const matchingValues = controlledVocabFields + .filter(fieldMatcher(queryTerm)) + .map(field => field.value) + return buildAtomicQueryByRelation({ fields, relation: 'any', terms: matchingValues, term, scope }) + }) + + const esOperator = (relation === 'any') ? 'should' : 'must' + return { + bool: { + [esOperator]: mappedQueries + } + } +} + function anyAllQueries ({ fields, relation, terms }) { const operator = (relation === 'any' ? 'should' : 'must') return { @@ -377,5 +409,6 @@ module.exports = { buildAtomicMain, nestedFilterAndMap, selectFields, - indexMapping + indexMapping, + mappedQueryForControlledVocabulary } diff --git a/test/cql_query_builder.test.js b/test/cql_query_builder.test.js index 88515494..d349316d 100644 --- a/test/cql_query_builder.test.js +++ b/test/cql_query_builder.test.js @@ -25,7 +25,11 @@ const { dateEnclosesQuery, filterQuery, multiAdjQuery, - exactMatchQuery + exactMatchQuery, + divisionAdj, + divisionAll, + divisionAny, + divisionExact } = require('./fixtures/cql_fixtures') describe('CQL Query Builder', function () { @@ -268,5 +272,33 @@ describe('CQL Query Builder', function () { expect(result).to.not.have.property('parsed') expect(result.error).to.include('Parsing error likely near end of') }) + + it('Maps controlled vocab fields correctly for any', () => { + const result = new CqlQuery('division any "manuscript art"').buildEsQuery() + expect(result).to.deep.equal( + divisionAny + ) + }) + + it('Maps controlled vocab fields correctly for all', () => { + const result = new CqlQuery('division all "manuscript art"').buildEsQuery() + expect(result).to.deep.equal( + divisionAll + ) + }) + + it('Maps controlled vocab fields correctly for adj', () => { + const result = new CqlQuery('division adj "manuscripts archives"').buildEsQuery() + expect(result).to.deep.equal( + divisionAdj + ) + }) + + it('Maps controlled vocab fields correctly for ==', () => { + const result = new CqlQuery('division == "mag"').buildEsQuery() + expect(result).to.deep.equal( + divisionExact + ) + }) }) }) diff --git a/test/fixtures/cql_fixtures.js b/test/fixtures/cql_fixtures.js index cce72afc..d72c5fb4 100644 --- a/test/fixtures/cql_fixtures.js +++ b/test/fixtures/cql_fixtures.js @@ -731,9 +731,61 @@ const binaryBooleanQuery = { should: [ { bool: { - should: [ - { term: { 'language.id': 'English' } }, - { term: { 'language.label': 'English' } } + must: [ + { + bool: { + should: [ + { + bool: { + should: [ + { + term: { 'language.id': 'lang:eng' } + }, + { + term: { 'language.label': 'lang:eng' } + } + ] + } + }, + { + bool: { + should: [ + { + term: { 'language.id': 'lang:enm' } + }, + { + term: { 'language.label': 'lang:enm' } + } + ] + } + }, + { + bool: { + should: [ + { + term: { 'language.id': 'lang:ang' } + }, + { + term: { 'language.label': 'lang:ang' } + } + ] + } + }, + { + bool: { + should: [ + { + term: { 'language.id': 'lang:cpe' } + }, + { + term: { 'language.label': 'lang:cpe' } + } + ] + } + } + ] + } + } ] } } @@ -786,9 +838,77 @@ const ternaryBooleanQuery = { should: [ { bool: { - should: [ - { term: { 'language.id': 'English' } }, - { term: { 'language.label': 'English' } } + must: [ + { + bool: { + should: [ + { + bool: { + should: [ + { + term: { + 'language.id': 'lang:eng' + } + }, + { + term: { + 'language.label': 'lang:eng' + } + } + ] + } + }, + { + bool: { + should: [ + { + term: { + 'language.id': 'lang:enm' + } + }, + { + term: { + 'language.label': 'lang:enm' + } + } + ] + } + }, + { + bool: { + should: [ + { + term: { + 'language.id': 'lang:ang' + } + }, + { + term: { + 'language.label': 'lang:ang' + } + } + ] + } + }, + { + bool: { + should: [ + { + term: { + 'language.id': 'lang:cpe' + } + }, + { + term: { + 'language.label': 'lang:cpe' + } + } + ] + } + } + ] + } + } ] } } @@ -863,9 +983,77 @@ const queryWithParentheses = { should: [ { bool: { - should: [ - { term: { 'language.id': 'English' } }, - { term: { 'language.label': 'English' } } + must: [ + { + bool: { + should: [ + { + bool: { + should: [ + { + term: { + 'language.id': 'lang:eng' + } + }, + { + term: { + 'language.label': 'lang:eng' + } + } + ] + } + }, + { + bool: { + should: [ + { + term: { + 'language.id': 'lang:enm' + } + }, + { + term: { + 'language.label': 'lang:enm' + } + } + ] + } + }, + { + bool: { + should: [ + { + term: { + 'language.id': 'lang:ang' + } + }, + { + term: { + 'language.label': 'lang:ang' + } + } + ] + } + }, + { + bool: { + should: [ + { + term: { + 'language.id': 'lang:cpe' + } + }, + { + term: { + 'language.label': 'lang:cpe' + } + } + ] + } + } + ] + } + } ] } } @@ -939,9 +1127,61 @@ const negationQuery = { should: [ { bool: { - should: [ - { term: { 'language.id': 'English' } }, - { term: { 'language.label': 'English' } } + must: [ + { + bool: { + should: [ + { + bool: { + should: [ + { + term: { 'language.id': 'lang:eng' } + }, + { + term: { 'language.label': 'lang:eng' } + } + ] + } + }, + { + bool: { + should: [ + { + term: { 'language.id': 'lang:enm' } + }, + { + term: { 'language.label': 'lang:enm' } + } + ] + } + }, + { + bool: { + should: [ + { + term: { 'language.id': 'lang:ang' } + }, + { + term: { 'language.label': 'lang:ang' } + } + ] + } + }, + { + bool: { + should: [ + { + term: { 'language.id': 'lang:cpe' } + }, + { + term: { 'language.label': 'lang:cpe' } + } + ] + } + } + ] + } + } ] } } @@ -1154,6 +1394,168 @@ const exactMatchQuery = { } } +const divisionAny = { + bool: { + must: [ + { + bool: { + should: [ + { + bool: { + should: [ + { + bool: { + should: [ + { + bool: { + should: [{ term: { collectionIds: 'mao' } }] + } + }, + { + bool: { + should: [{ term: { collectionIds: 'scd' } }] + } + } + ] + } + }, + { + bool: { + should: [ + { + bool: { + should: [{ term: { collectionIds: 'mab' } }] + } + }, + { + bool: { + should: [{ term: { collectionIds: 'scc' } }] + } + } + ] + } + } + ] + } + } + ] + } + } + ] + } +} + +const divisionAll = { + bool: { + must: [ + { + bool: { + should: [ + { + bool: { + must: [ + { + bool: { + should: [ + { + bool: { + should: [{ term: { collectionIds: 'mao' } }] + } + }, + { + bool: { + should: [{ term: { collectionIds: 'scd' } }] + } + } + ] + } + }, + { + bool: { + should: [ + { + bool: { + should: [{ term: { collectionIds: 'mab' } }] + } + }, + { + bool: { + should: [{ term: { collectionIds: 'scc' } }] + } + } + ] + } + } + ] + } + } + ] + } + } + ] + } +} + +const divisionAdj = { + bool: { + must: [ + { + bool: { + should: [ + { + bool: { + must: [ + { + bool: { + should: [ + { + bool: { + should: [{ term: { collectionIds: 'scd' } }] + } + } + ] + } + } + ] + } + } + ] + } + } + ] + } +} + +const divisionExact = { + bool: { + must: [ + { + bool: { + should: [ + { + bool: { + must: [ + { + bool: { + should: [ + { + bool: { + should: [{ term: { collectionIds: 'mag' } }] + } + } + ] + } + } + ] + } + } + ] + } + } + ] + } +} + module.exports = { simpleAdjQuery, simpleAnyQuery, @@ -1176,5 +1578,9 @@ module.exports = { dateEnclosesQuery, filterQuery, multiAdjQuery, - exactMatchQuery + exactMatchQuery, + divisionAdj, + divisionAll, + divisionAny, + divisionExact } From 9f71653434691717d8408ec319f10a514178c766 Mon Sep 17 00:00:00 2001 From: danamansana Date: Wed, 15 Apr 2026 14:10:02 -0400 Subject: [PATCH 2/7] &ai add new logic for handling date queries + tests --- lib/elasticsearch/cql_query_builder.js | 85 ++++-- lib/elasticsearch/cql_query_builder.test.js | 0 test/cql_query_builder_dates.test.js | 306 ++++++++++++++++++++ 3 files changed, 368 insertions(+), 23 deletions(-) create mode 100644 lib/elasticsearch/cql_query_builder.test.js create mode 100644 test/cql_query_builder_dates.test.js diff --git a/lib/elasticsearch/cql_query_builder.js b/lib/elasticsearch/cql_query_builder.js index 02fc6d34..b66b349c 100644 --- a/lib/elasticsearch/cql_query_builder.js +++ b/lib/elasticsearch/cql_query_builder.js @@ -239,22 +239,19 @@ function buildAtomicQueryByRelation ({ fields, relation, terms, term }) { case '==': case 'adj': return adjEqQueries({ fields, relation, terms, term }) - case '>': - case '<': - case '<=': - case '>=': - case 'within': - case 'encloses': - return dateQueries({ fields, relation, terms, term }) default: break } } function buildAtomicMain ({ fields, relation, terms, term, scope }) { - return !Object.keys(controlledVocabularies).includes(scope) - ? buildAtomicQueryByRelation({ fields, relation, terms, term }) - : mappedQueryForControlledVocabulary({ fields, relation, terms, term, scope }) + if (Object.keys(controlledVocabularies).includes(scope)) { + return mappedQueryForControlledVocabulary({ fields, relation, terms, term, scope }) + } + if (scope === 'date') { + return dateQueries({ fields, relation, terms, term }) + } + return buildAtomicQueryByRelation({ fields, relation, terms, term }) } function mappedQueryForControlledVocabulary ({ fields, relation, terms, term, scope }) { @@ -343,40 +340,82 @@ function matchTermWithFields (fields, term, type) { } } -function dateQueries ({ fields, relation, terms, term }) { - if (!Object.values(fields).some(fieldType => fieldType.some(field => field.includes('date')))) { return null } +function convertSingleDateToRange (date) { + const dateReg = /^(\d{4})(?:[-/](\d{2}))?(?:[-/](\d{2}))?$/ + const match = date.match(dateReg) + let rangeEnd + if (match[3]) { + rangeEnd = { lte: `${match[1]}-${match[2]}-${match[3]}T23:59:59` } + } else if (match[2] && match[2] !== '12') { + rangeEnd = { lt: `${match[1]}-${parseInt(match[2], 10) + 1}` } + } else { + rangeEnd = { lt: `${parseInt(match[1], 10) + 1}` } + } + return Object.assign({ gte: date, relation: 'within' }, rangeEnd) +} + +function rangeQueryForDates ({ relation, queryTerms }) { let range switch (relation) { case '<': - range = { lt: terms[0] } + range = { lt: queryTerms[0] } break case '>': - range = { gt: terms[0] } + range = { gt: queryTerms[0] } break case '>=': - range = { gte: terms[0] } + range = { gte: queryTerms[0] } break case '<=': - range = { lte: terms[0] } + range = { lte: queryTerms[0] } break case 'encloses': - range = { gt: terms[0], lt: terms[1] } + range = { gt: queryTerms[0], lt: queryTerms[1] } break case 'within': - range = { gte: terms[0], lte: terms[1] } + range = { gte: queryTerms[0], lte: queryTerms[1] } break default: + range = convertSingleDateToRange(queryTerms[0]) break } return { - nested: { - path: 'dates', - query: { - range: { - 'dates.range': range + range: { + 'dates.range': range + } + } +} + +function dateQueries ({ fields, relation, terms, term }) { + if (!Object.values(fields).some(fieldType => fieldType.some(field => field.includes('date')))) { return null } + + let query + + switch (relation) { + case 'any': + query = { + bool: { + should: terms.map(queryTerm => rangeQueryForDates({ relation, queryTerms: [queryTerm] })) } } + break + case 'all': + query = { + bool: { + must: terms.map(queryTerm => rangeQueryForDates({ relation, queryTerms: [queryTerm] })) + } + } + break + default: + query = rangeQueryForDates({ relation, queryTerms: terms }) + break + } + + return { + nested: { + path: 'dates', + query } } } diff --git a/lib/elasticsearch/cql_query_builder.test.js b/lib/elasticsearch/cql_query_builder.test.js new file mode 100644 index 00000000..e69de29b diff --git a/test/cql_query_builder_dates.test.js b/test/cql_query_builder_dates.test.js new file mode 100644 index 00000000..2c5c8940 --- /dev/null +++ b/test/cql_query_builder_dates.test.js @@ -0,0 +1,306 @@ +const { expect } = require('chai') +const { buildAtomicMain } = require('../lib/elasticsearch/cql_query_builder') + +describe('cql_query_builder date queries', () => { + // Mocking fields per `indexMapping.date` + const dateFields = { fields: ['dates.range'] } + + it('builds range queries for relation "any" with multiple dates', () => { + const query = buildAtomicMain({ + scope: 'date', + relation: 'any', + terms: ['1999', '2000'], + term: '1999 2000', + fields: dateFields + }) + + expect(query).to.deep.equal({ + nested: { + path: 'dates', + query: { + bool: { + should: [ + { + range: { + 'dates.range': { gte: '1999', relation: 'within', lt: '2000' } + } + }, + { + range: { + 'dates.range': { gte: '2000', relation: 'within', lt: '2001' } + } + } + ] + } + } + } + }) + }) + + it('builds range queries for relation "all" with multiple dates', () => { + const query = buildAtomicMain({ + scope: 'date', + relation: 'all', + terms: ['1999', '2000'], + term: '1999 2000', + fields: dateFields + }) + + expect(query).to.deep.equal({ + nested: { + path: 'dates', + query: { + bool: { + must: [ + { + range: { + 'dates.range': { gte: '1999', relation: 'within', lt: '2000' } + } + }, + { + range: { + 'dates.range': { gte: '2000', relation: 'within', lt: '2001' } + } + } + ] + } + } + } + }) + }) + + it('builds range query for relation "within" connecting two bounds', () => { + const query = buildAtomicMain({ + scope: 'date', + relation: 'within', + terms: ['1990', '2000'], + term: '1990 2000', + fields: dateFields + }) + + expect(query).to.deep.equal({ + nested: { + path: 'dates', + query: { + range: { + 'dates.range': { gte: '1990', lte: '2000' } + } + } + } + }) + }) + + it('builds range query for basic mathematical relation "<"', () => { + const query = buildAtomicMain({ + scope: 'date', + relation: '<', + terms: ['1999'], + term: '1999', + fields: dateFields + }) + + expect(query).to.deep.equal({ + nested: { + path: 'dates', + query: { + range: { + 'dates.range': { lt: '1999' } + } + } + } + }) + }) + + it('builds range query for basic mathematical relation ">"', () => { + const query = buildAtomicMain({ + scope: 'date', + relation: '>', + terms: ['1999'], + term: '1999', + fields: dateFields + }) + + expect(query).to.deep.equal({ + nested: { + path: 'dates', + query: { + range: { + 'dates.range': { gt: '1999' } + } + } + } + }) + }) + + it('builds range query for basic mathematical relation ">="', () => { + const query = buildAtomicMain({ + scope: 'date', + relation: '>=', + terms: ['1999'], + term: '1999', + fields: dateFields + }) + + expect(query).to.deep.equal({ + nested: { + path: 'dates', + query: { + range: { + 'dates.range': { gte: '1999' } + } + } + } + }) + }) + + it('builds range query for basic mathematical relation "<="', () => { + const query = buildAtomicMain({ + scope: 'date', + relation: '<=', + terms: ['1999'], + term: '1999', + fields: dateFields + }) + + expect(query).to.deep.equal({ + nested: { + path: 'dates', + query: { + range: { + 'dates.range': { lte: '1999' } + } + } + } + }) + }) + + it('builds range query for relation "encloses" connecting two bounds', () => { + const query = buildAtomicMain({ + scope: 'date', + relation: 'encloses', + terms: ['1990', '2000'], + term: '1990 2000', + fields: dateFields + }) + + expect(query).to.deep.equal({ + nested: { + path: 'dates', + query: { + range: { + 'dates.range': { gt: '1990', lt: '2000' } + } + } + } + }) + }) + + it('builds range query for relation "=" with yyyy date', () => { + const query = buildAtomicMain({ + scope: 'date', + relation: '=', + terms: ['1999'], + term: '1999', + fields: dateFields + }) + + expect(query).to.deep.equal({ + nested: { + path: 'dates', + query: { + range: { + 'dates.range': { gte: '1999', relation: 'within', lt: '2000' } + } + } + } + }) + }) + + it('builds range query for relation "=" with yyyy-mm date', () => { + const query = buildAtomicMain({ + scope: 'date', + relation: '=', + terms: ['1999-10'], + term: '1999-10', + fields: dateFields + }) + + expect(query).to.deep.equal({ + nested: { + path: 'dates', + query: { + range: { + 'dates.range': { gte: '1999-10', relation: 'within', lt: '1999-11' } + } + } + } + }) + }) + + it('builds range query for relation "=" with yyyy-mm-dd date', () => { + const query = buildAtomicMain({ + scope: 'date', + relation: '=', + terms: ['1999-10-15'], + term: '1999-10-15', + fields: dateFields + }) + + expect(query).to.deep.equal({ + nested: { + path: 'dates', + query: { + range: { + 'dates.range': { gte: '1999-10-15', relation: 'within', lte: '1999-10-15T23:59:59' } + } + } + } + }) + }) + + it('builds range query for relation "=" with yyyy-12 date', () => { + const query = buildAtomicMain({ + scope: 'date', + relation: '=', + terms: ['1999-12'], + term: '1999-12', + fields: dateFields + }) + + expect(query).to.deep.equal({ + nested: { + path: 'dates', + query: { + range: { + 'dates.range': { gte: '1999-12', relation: 'within', lt: '2000' } + } + } + } + }) + }) + + it('builds range query for relation "=" with yyyy-mm-dd date at end of month', () => { + const query = buildAtomicMain({ + scope: 'date', + relation: '=', + terms: ['2023-04-30'], + term: '2023-04-30', + fields: dateFields + }) + + expect(query).to.deep.equal({ + nested: { + path: 'dates', + query: { + range: { + 'dates.range': { + gte: '2023-04-30', + relation: 'within', + lte: '2023-04-30T23:59:59' + } + } + } + } + }) + }) +}) From a3d949e385fca30d324414fc32a5e8293d000308 Mon Sep 17 00:00:00 2001 From: danamansana Date: Wed, 15 Apr 2026 14:17:22 -0400 Subject: [PATCH 3/7] Remove empty file --- lib/elasticsearch/cql_query_builder.test.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 lib/elasticsearch/cql_query_builder.test.js diff --git a/lib/elasticsearch/cql_query_builder.test.js b/lib/elasticsearch/cql_query_builder.test.js deleted file mode 100644 index e69de29b..00000000 From 04e1fe625b0ddc239ac90e7bb4ea4a11408176e0 Mon Sep 17 00:00:00 2001 From: danamansana Date: Wed, 15 Apr 2026 14:56:03 -0400 Subject: [PATCH 4/7] Add tag requirement for exact date queries --- lib/elasticsearch/cql_query_builder.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/elasticsearch/cql_query_builder.js b/lib/elasticsearch/cql_query_builder.js index b66b349c..87ce6bdd 100644 --- a/lib/elasticsearch/cql_query_builder.js +++ b/lib/elasticsearch/cql_query_builder.js @@ -412,6 +412,21 @@ function dateQueries ({ fields, relation, terms, term }) { break } + if (['=', '==', 'adj', 'any', 'all'].includes(relation)) { + query = { + bool: { + must: [ + query, + { + terms: { + 'dates.tag': ['e', 's', 'p', 'r', 't'] + } + } + ] + } + } + } + return { nested: { path: 'dates', From da0f1573a6befaec14b5a1a5b883b83397e29fd1 Mon Sep 17 00:00:00 2001 From: danamansana Date: Wed, 15 Apr 2026 14:59:33 -0400 Subject: [PATCH 5/7] &ai update tests --- test/cql_query_builder_dates.test.js | 123 ++++++++++++++++++++++----- 1 file changed, 100 insertions(+), 23 deletions(-) diff --git a/test/cql_query_builder_dates.test.js b/test/cql_query_builder_dates.test.js index 2c5c8940..c6a2b564 100644 --- a/test/cql_query_builder_dates.test.js +++ b/test/cql_query_builder_dates.test.js @@ -19,15 +19,26 @@ describe('cql_query_builder date queries', () => { path: 'dates', query: { bool: { - should: [ + must: [ { - range: { - 'dates.range': { gte: '1999', relation: 'within', lt: '2000' } + bool: { + should: [ + { + range: { + 'dates.range': { gte: '1999', relation: 'within', lt: '2000' } + } + }, + { + range: { + 'dates.range': { gte: '2000', relation: 'within', lt: '2001' } + } + } + ] } }, { - range: { - 'dates.range': { gte: '2000', relation: 'within', lt: '2001' } + terms: { + 'dates.tag': ['e', 's', 'p', 'r', 't'] } } ] @@ -53,13 +64,24 @@ describe('cql_query_builder date queries', () => { bool: { must: [ { - range: { - 'dates.range': { gte: '1999', relation: 'within', lt: '2000' } + bool: { + must: [ + { + range: { + 'dates.range': { gte: '1999', relation: 'within', lt: '2000' } + } + }, + { + range: { + 'dates.range': { gte: '2000', relation: 'within', lt: '2001' } + } + } + ] } }, { - range: { - 'dates.range': { gte: '2000', relation: 'within', lt: '2001' } + terms: { + 'dates.tag': ['e', 's', 'p', 'r', 't'] } } ] @@ -208,8 +230,19 @@ describe('cql_query_builder date queries', () => { nested: { path: 'dates', query: { - range: { - 'dates.range': { gte: '1999', relation: 'within', lt: '2000' } + bool: { + must: [ + { + range: { + 'dates.range': { gte: '1999', relation: 'within', lt: '2000' } + } + }, + { + terms: { + 'dates.tag': ['e', 's', 'p', 'r', 't'] + } + } + ] } } } @@ -229,8 +262,19 @@ describe('cql_query_builder date queries', () => { nested: { path: 'dates', query: { - range: { - 'dates.range': { gte: '1999-10', relation: 'within', lt: '1999-11' } + bool: { + must: [ + { + range: { + 'dates.range': { gte: '1999-10', relation: 'within', lt: '1999-11' } + } + }, + { + terms: { + 'dates.tag': ['e', 's', 'p', 'r', 't'] + } + } + ] } } } @@ -250,8 +294,19 @@ describe('cql_query_builder date queries', () => { nested: { path: 'dates', query: { - range: { - 'dates.range': { gte: '1999-10-15', relation: 'within', lte: '1999-10-15T23:59:59' } + bool: { + must: [ + { + range: { + 'dates.range': { gte: '1999-10-15', relation: 'within', lte: '1999-10-15T23:59:59' } + } + }, + { + terms: { + 'dates.tag': ['e', 's', 'p', 'r', 't'] + } + } + ] } } } @@ -271,8 +326,19 @@ describe('cql_query_builder date queries', () => { nested: { path: 'dates', query: { - range: { - 'dates.range': { gte: '1999-12', relation: 'within', lt: '2000' } + bool: { + must: [ + { + range: { + 'dates.range': { gte: '1999-12', relation: 'within', lt: '2000' } + } + }, + { + terms: { + 'dates.tag': ['e', 's', 'p', 'r', 't'] + } + } + ] } } } @@ -292,12 +358,23 @@ describe('cql_query_builder date queries', () => { nested: { path: 'dates', query: { - range: { - 'dates.range': { - gte: '2023-04-30', - relation: 'within', - lte: '2023-04-30T23:59:59' - } + bool: { + must: [ + { + range: { + 'dates.range': { + gte: '2023-04-30', + relation: 'within', + lte: '2023-04-30T23:59:59' + } + } + }, + { + terms: { + 'dates.tag': ['e', 's', 'p', 'r', 't'] + } + } + ] } } } From 594fb6acf19576f8d356548e920b1cd01b0e7079 Mon Sep 17 00:00:00 2001 From: danamansana Date: Wed, 15 Apr 2026 16:24:47 -0400 Subject: [PATCH 6/7] Add ControlledVocabularies module: --- app.js | 3 ++ lib/elasticsearch/cql_query_builder.js | 2 +- lib/models/ControlledVocabularies.js | 48 +++++++++++++++++++ .../fixtures}/controlledVocabularies.json | 0 test/test_helper.js | 4 ++ 5 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 lib/models/ControlledVocabularies.js rename {lib/elasticsearch/cql => test/fixtures}/controlledVocabularies.json (100%) diff --git a/app.js b/app.js index 571f56a7..b426c511 100644 --- a/app.js +++ b/app.js @@ -1,5 +1,6 @@ const express = require('express') const NyplSourceMapper = require('research-catalog-indexer/lib/utils/nypl-source-mapper') +const ControlledVocabularies = require('./lib/models/ControlledVocabularies') const esClient = require('./lib/elasticsearch/client') const loadConfig = require('./lib/load-config') @@ -70,6 +71,8 @@ app.init = async () => { handleError(err, req, res, next, app.logger) }) + await ControlledVocabularies.initialize(); + return app } diff --git a/lib/elasticsearch/cql_query_builder.js b/lib/elasticsearch/cql_query_builder.js index 02fc6d34..aa8d504c 100644 --- a/lib/elasticsearch/cql_query_builder.js +++ b/lib/elasticsearch/cql_query_builder.js @@ -2,7 +2,7 @@ const { parseWithRightCql, reverseString, parsedASTtoNestedArray } = require('./ const { indexMapping } = require('./cql/index-mapping') const ElasticQueryBuilder = require('./elastic-query-builder') const { InvalidParameterError } = require('../errors') -const controlledVocabularies = require('./cql/controlledVocabularies.json') +const controlledVocabularies = require('../models/ControlledVocabularies').getData() class CqlQuery { constructor (queryStr) { diff --git a/lib/models/ControlledVocabularies.js b/lib/models/ControlledVocabularies.js new file mode 100644 index 00000000..d3b003da --- /dev/null +++ b/lib/models/ControlledVocabularies.js @@ -0,0 +1,48 @@ +class ControlledVocabularies { + // We'll store the promise here + static fetchedVocabularies = null; + + /** + * Initializes the fetch request. Call this once during app startup. + */ + static async initialize() { + if (!this.fetchedVocabularies) { + // Assign the promise itself to the variable + this.fetchedVocabularies = fetch(process.env.vocabulariesEndpoint) + .then(async response => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); + return { + format: data.formats, + language: data.languages, + center: data.buildingLocations, + division: data.collections + }; + }) + .catch(error => { + // If it fails, clear the promise so we can try again next time + this.fetchedVocabularies = null; + throw error; + }); + } + + return this.fetchedVocabularies; + } + + /** + * Synchronously grab the data if you are absolutely sure it has resolved, + * or await the promise to be safe. + */ + static async getData() { + if (!this.fetchedVocabularies) { + throw new Error('ControlledVocabularies has not been initialized. Call initialize() first.'); + } + + // Returning the promise means callers can safely await it + return this.fetchedVocabularies; + } +} + +module.exports = ControlledVocabularies; diff --git a/lib/elasticsearch/cql/controlledVocabularies.json b/test/fixtures/controlledVocabularies.json similarity index 100% rename from lib/elasticsearch/cql/controlledVocabularies.json rename to test/fixtures/controlledVocabularies.json diff --git a/test/test_helper.js b/test/test_helper.js index eac2a22c..372e19e3 100644 --- a/test/test_helper.js +++ b/test/test_helper.js @@ -4,6 +4,7 @@ const dotenv = require('dotenv') const loadConfig = require('../lib/load-config') const app = require('../app') const sinon = require('sinon') +const ControlledVocabularies = require('../lib/models/ControlledVocabularies') before(async () => { if (!process.env.UPDATE_FIXTURES) { @@ -28,6 +29,9 @@ before(async () => { process.env.AWS_PROFILE = process.env.AWS_PROFILE || 'nypl-digital-dev' } + ControlledVocabularies.fetchedVocabularies = Promise.resolve(require('./fixtures/controlledVocabularies.json')) + ControlledVocabularies.initialize() + await app.init() // Establish base url for local queries: From f634ecf329b3c2efb152c661b03d8f241b5c62c1 Mon Sep 17 00:00:00 2001 From: danamansana Date: Thu, 16 Apr 2026 14:57:25 -0400 Subject: [PATCH 7/7] Add sync ControlledVocabularies caching vocabularies for CQL --- app.js | 2 +- lib/elasticsearch/cql_query_builder.js | 11 +++--- lib/models/ControlledVocabularies.js | 47 +++++++++++++++----------- test/cql_query_builder.test.js | 6 ++++ test/test_helper.js | 2 +- 5 files changed, 41 insertions(+), 27 deletions(-) diff --git a/app.js b/app.js index b426c511..cef2fc37 100644 --- a/app.js +++ b/app.js @@ -71,7 +71,7 @@ app.init = async () => { handleError(err, req, res, next, app.logger) }) - await ControlledVocabularies.initialize(); + await ControlledVocabularies.initialize(app) return app } diff --git a/lib/elasticsearch/cql_query_builder.js b/lib/elasticsearch/cql_query_builder.js index aa8d504c..a87066fd 100644 --- a/lib/elasticsearch/cql_query_builder.js +++ b/lib/elasticsearch/cql_query_builder.js @@ -2,7 +2,7 @@ const { parseWithRightCql, reverseString, parsedASTtoNestedArray } = require('./ const { indexMapping } = require('./cql/index-mapping') const ElasticQueryBuilder = require('./elastic-query-builder') const { InvalidParameterError } = require('../errors') -const controlledVocabularies = require('../models/ControlledVocabularies').getData() +const ControlledVocabularies = require('../models/ControlledVocabularies') class CqlQuery { constructor (queryStr) { @@ -252,15 +252,16 @@ function buildAtomicQueryByRelation ({ fields, relation, terms, term }) { } function buildAtomicMain ({ fields, relation, terms, term, scope }) { - return !Object.keys(controlledVocabularies).includes(scope) + const vocabularies = ControlledVocabularies.getCachedData() || {} + return !Object.keys(vocabularies).includes(scope) ? buildAtomicQueryByRelation({ fields, relation, terms, term }) - : mappedQueryForControlledVocabulary({ fields, relation, terms, term, scope }) + : mappedQueryForControlledVocabulary({ fields, relation, terms, term, scope, vocabularies }) } -function mappedQueryForControlledVocabulary ({ fields, relation, terms, term, scope }) { +function mappedQueryForControlledVocabulary ({ fields, relation, terms, term, scope, vocabularies }) { // If relation is any/all, termsToMap should be terms, otherwise it should be [term] const termsToMap = ['any', 'all'].includes(relation) ? terms : [term] - const controlledVocabFields = controlledVocabularies[scope] + const controlledVocabFields = vocabularies[scope] const normalizer = str => str.toLowerCase().replace(/[^a-zA-Z0-9 ]/g, '') const fieldMatcher = relation === '==' ? (queryTerm) => field => field.value === queryTerm diff --git a/lib/models/ControlledVocabularies.js b/lib/models/ControlledVocabularies.js index d3b003da..f0b6ea64 100644 --- a/lib/models/ControlledVocabularies.js +++ b/lib/models/ControlledVocabularies.js @@ -1,48 +1,55 @@ class ControlledVocabularies { // We'll store the promise here - static fetchedVocabularies = null; + static fetchedVocabularies = null + // We'll store the synchronously accessible data here + static cachedData = {} /** * Initializes the fetch request. Call this once during app startup. */ - static async initialize() { + static async initialize (app) { if (!this.fetchedVocabularies) { // Assign the promise itself to the variable - this.fetchedVocabularies = fetch(process.env.vocabulariesEndpoint) - .then(async response => { - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - const data = await response.json(); - return { + this.fetchedVocabularies = app.vocabularies({}, {}) + .then(data => { + this.cachedData = { format: data.formats, language: data.languages, center: data.buildingLocations, division: data.collections - }; + } + return this.cachedData }) .catch(error => { // If it fails, clear the promise so we can try again next time - this.fetchedVocabularies = null; - throw error; - }); + this.fetchedVocabularies = null + throw error + }) } - - return this.fetchedVocabularies; + + return this.fetchedVocabularies } /** * Synchronously grab the data if you are absolutely sure it has resolved, * or await the promise to be safe. */ - static async getData() { + static async getData () { if (!this.fetchedVocabularies) { - throw new Error('ControlledVocabularies has not been initialized. Call initialize() first.'); + throw new Error('ControlledVocabularies has not been initialized. Call initialize() first.') } - + // Returning the promise means callers can safely await it - return this.fetchedVocabularies; + return this.fetchedVocabularies + } + + /** + * Synchronously return the cached data. Use this in synchronous functions + * like cql_query_builder that cannot await the Promise. + */ + static getCachedData () { + return this.cachedData } } -module.exports = ControlledVocabularies; +module.exports = ControlledVocabularies diff --git a/test/cql_query_builder.test.js b/test/cql_query_builder.test.js index d349316d..1d29b06e 100644 --- a/test/cql_query_builder.test.js +++ b/test/cql_query_builder.test.js @@ -3,6 +3,8 @@ const { expect } = require('chai') const { CqlQuery } = require('../lib/elasticsearch/cql_query_builder') const ApiRequest = require('../lib/api-request') const { InvalidParameterError } = require('../lib/errors') +const ControlledVocabularies = require('../lib/models/ControlledVocabularies') +const vocabFixture = require('./fixtures/controlledVocabularies.json') const { simpleAdjQuery, simpleAnyQuery, @@ -33,6 +35,10 @@ const { } = require('./fixtures/cql_fixtures') describe('CQL Query Builder', function () { + before(() => { + ControlledVocabularies.cachedData = vocabFixture + }) + it('Simple = query', function () { expect(new CqlQuery('title="Hamlet"').buildEsQuery()) .to.deep.equal( diff --git a/test/test_helper.js b/test/test_helper.js index 372e19e3..3a852cd0 100644 --- a/test/test_helper.js +++ b/test/test_helper.js @@ -30,7 +30,7 @@ before(async () => { } ControlledVocabularies.fetchedVocabularies = Promise.resolve(require('./fixtures/controlledVocabularies.json')) - ControlledVocabularies.initialize() + ControlledVocabularies.initialize() await app.init()