From 6642181ce126226a80cae88ab993b6ab8c09622c Mon Sep 17 00:00:00 2001 From: Ross Date: Sat, 11 Jul 2026 11:01:57 +0100 Subject: [PATCH] update search --- results.json | 719 ++++++++++++++++++++++++++++------------- tools/search_md.py | 254 +++++++++------ tools/search_md_gui.py | 137 +++++--- 3 files changed, 732 insertions(+), 378 deletions(-) diff --git a/results.json b/results.json index 39a3066..f84405a 100644 --- a/results.json +++ b/results.json @@ -1,94 +1,139 @@ [ { - "path": "docs_md/articles/facial-nerve-in-temporal-bone_21dccac8-d73d-4ef3-859b-73e013ec15cc.md", - "title": "Facial Nerve in Temporal Bone", - "docid": "21dccac8-d73d-4ef3-859b-73e013ec15cc", + "path": "docs_md/articles/adrenal_082ca43c-db5c-4770-aeed-0c6ea317e8fc.md", + "title": "Adrenal", + "docid": "082ca43c-db5c-4770-aeed-0c6ea317e8fc", "breadcrumbs": [ - "Head and Neck", + "Genitourinary", "Anatomy", - "Temporal Bone", - "Facial Nerve in Temporal Bone" + "Adrenal" ], "authors": [ { - "key": "b2e6dabb-ee1c-42a4-a332-9f0814c1c607", - "value": "Surjith Vattoth, MD, FRCR" + "key": "c1df94ab-4a9f-44c4-add7-1f174fb9ac45", + "value": "Siva P. Raman, MD" } ], - "pageKeywords": "Head and Neck, Anatomy, Temporal Bone, Facial Nerve in Temporal Bone", + "pageKeywords": "Genitourinary, Anatomy, Adrenal", "reasons": [ - "Breadcrumbs", - "Keywords", - "Title" + "Content" ], - "snippet": "" + "snippet": "...hrine) into bloodstream - **Vessels**,**nerves**, and **lymphatics** - Arteries - **Superior adrenal arteries**: Typic..." }, { - "path": "docs_md/articles/cnvii-facial-nerve_98cb2d45-e64c-4295-9662-3470cd46513a.md", - "title": "CNVII (Facial Nerve)", - "docid": "98cb2d45-e64c-4295-9662-3470cd46513a", - "breadcrumbs": [ - "Head and Neck", - "Anatomy", - "Cranial Nerves", - "CNVII (Facial Nerve)" - ], - "authors": [ - { - "key": "1fa14dfd-71ea-4960-908e-e720313bc63a", - "value": "Santhosh Gaddikeri, MD" - }, - { - "key": "94f835c8-fa13-4e8a-995b-53048e6b0605", - "value": "Philip R. Chapman, MD" - } - ], - "pageKeywords": "Head and Neck, Anatomy, Cranial Nerves, CNVII (Facial Nerve)", - "reasons": [ - "Breadcrumbs", - "Keywords", - "Title" - ], - "snippet": "" - }, - { - "path": "docs_md/articles/facial-nerve-cnvii_2f4818dd-6438-405b-8561-5cbbb9c91562.md", - "title": "Facial Nerve (CNVII)", - "docid": "2f4818dd-6438-405b-8561-5cbbb9c91562", - "breadcrumbs": [ - "Brain", - "Anatomy", - "Skull Base and Cranial Nerves", - "Facial Nerve (CNVII)" - ], - "authors": [ - { - "key": "94f835c8-fa13-4e8a-995b-53048e6b0605", - "value": "Philip R. Chapman, MD" - }, - { - "key": "b0a6efa4-ad68-430c-b5da-f5c904adf809", - "value": "Ryan P. Cabeen, PhD" - } - ], - "pageKeywords": "Brain, Anatomy, Skull Base and Cranial Nerves, Facial Nerve (CNVII)", - "reasons": [ - "Breadcrumbs", - "Keywords", - "Title" - ], - "snippet": "" - }, - { - "path": "docs_md/articles/peripheral-facial-nerve-paralysis_4da52ac4-c03c-4711-ae7e-bb4f2f7c5ab8.md", - "title": "Peripheral Facial Nerve Paralysis", - "docid": "4da52ac4-c03c-4711-ae7e-bb4f2f7c5ab8", + "path": "docs_md/articles/sensorineural-hearing-loss-in-child_08c895da-f2aa-4076-abf0-af9aca1677cd.md", + "title": "Sensorineural Hearing Loss in Child", + "docid": "08c895da-f2aa-4076-abf0-af9aca1677cd", "breadcrumbs": [ "Head and Neck", "Differential Diagnosis", - "Temporal Bone", + "CPA-IAC and Posterior Fossa", "Clinically Based Differentials", - "Peripheral Facial Nerve Paralysis" + "Sensorineural Hearing Loss in Child" + ], + "authors": [ + { + "key": "d19354f3-7ff2-495a-ad3f-064122e45602", + "value": "Bernadette L. Koch, MD" + } + ], + "pageKeywords": "Head and Neck, Differential Diagnosis, CPA-IAC and Posterior Fossa, Clinically Based Differentials, Sensorineural Hearing Loss in Child", + "reasons": [ + "Content" + ], + "snippet": "...lar deficiency on CT - Look for large endolymphatic sac/duct ± cochlear malformation & modiolar deficiency on MR - **Conge..." + }, + { + "path": "docs_md/articles/segmental-colonic-narrowing_c193bc86-43d2-4932-8378-5db8001d755e.md", + "title": "Segmental Colonic Narrowing", + "docid": "c193bc86-43d2-4932-8378-5db8001d755e", + "breadcrumbs": [ + "Gastrointestinal", + "Differential Diagnosis", + "Colon", + "Generic Imaging Patterns", + "Segmental Colonic Narrowing" + ], + "authors": [ + { + "key": "6c5a9e0e-9dea-461b-9ad4-c00f5c4c2bbf", + "value": "Atif Zaheer, MD, FSAR" + }, + { + "key": "e987d3d3-1206-48d6-824b-3347c2968855", + "value": "Michael P. Federle, MD, FACR" + } + ], + "pageKeywords": "Gastrointestinal, Differential Diagnosis, Colon, Generic Imaging Patterns, Segmental Colonic Narrowing", + "reasons": [ + "Content" + ], + "snippet": "...etastases and Lymphoma** - Can be hematogenous or lymphatic spread or direct invasion - e.g., from duodenum, stomach, kidn..." + }, + { + "path": "docs_md/articles/normal-pressure-hydrocephalus_ba3f857d-58de-4f21-8463-1631b4cb9972.md", + "title": "Normal-Pressure Hydrocephalus", + "docid": "ba3f857d-58de-4f21-8463-1631b4cb9972", + "breadcrumbs": [ + "Brain", + "Diagnosis", + "Anatomy-Based Diagnoses", + "Ventricles and Cisterns", + "Hydrocephalus", + "Normal-Pressure Hydrocephalus" + ], + "authors": [ + { + "key": "a25c450b-3d34-4f64-bba3-cc0834813df6", + "value": "Miral D. Jhaveri, MD, MBA" + } + ], + "pageKeywords": "Brain, Diagnosis, Anatomy-Based Diagnoses, Ventricles and Cisterns, Hydrocephalus, Normal-Pressure Hydrocephalus", + "reasons": [ + "Content" + ], + "snippet": "...oval of neurotoxic compounds (β-amyloid & tau) - Glymphatic system - Facilitates brain fluid clearance & waste removal du..." + }, + { + "path": "docs_md/articles/peritoneal-cavity_6691f48d-ac34-477b-8ec1-b9dd731a14a8.md", + "title": "Peritoneal Cavity", + "docid": "6691f48d-ac34-477b-8ec1-b9dd731a14a8", + "breadcrumbs": [ + "Gastrointestinal", + "Anatomy", + "Gastrointestinal Tract and Abdominal Cavity", + "Peritoneal Cavity" + ], + "authors": [ + { + "key": "6c5a9e0e-9dea-461b-9ad4-c00f5c4c2bbf", + "value": "Atif Zaheer, MD, FSAR" + }, + { + "key": "c1df94ab-4a9f-44c4-add7-1f174fb9ac45", + "value": "Siva P. Raman, MD" + }, + { + "key": "e987d3d3-1206-48d6-824b-3347c2968855", + "value": "Michael P. Federle, MD, FACR" + } + ], + "pageKeywords": "Gastrointestinal, Anatomy, Gastrointestinal Tract and Abdominal Cavity, Peritoneal Cavity", + "reasons": [ + "Content" + ], + "snippet": "...ntains **superior mesenteric artery and vein**, nerves, and lymphatics - **Transverse mesocolon**crosses almost horizontally in front of pancreas,..." + }, + { + "path": "docs_md/articles/sensorineural-hearing-loss-in-adult_08d468da-fbc3-44f8-8212-6480e0a152c4.md", + "title": "Sensorineural Hearing Loss in Adult", + "docid": "08d468da-fbc3-44f8-8212-6480e0a152c4", + "breadcrumbs": [ + "Head and Neck", + "Differential Diagnosis", + "CPA-IAC and Posterior Fossa", + "Clinically Based Differentials", + "Sensorineural Hearing Loss in Adult" ], "authors": [ { @@ -96,13 +141,33 @@ "value": "Nicholas A. Koontz, MD" } ], - "pageKeywords": "Head and Neck, Differential Diagnosis, Temporal Bone, Clinically Based Differentials, Peripheral Facial Nerve Paralysis", + "pageKeywords": "Head and Neck, Differential Diagnosis, CPA-IAC and Posterior Fossa, Clinically Based Differentials, Sensorineural Hearing Loss in Adult", "reasons": [ - "Breadcrumbs", - "Keywords", - "Title" + "Content" ], - "snippet": "" + "snippet": "...ove; fat saturation suppresses fat signal - **Large Endolymphatic Sac Anomaly (IP-2)** - Most common lesion found in children with bilat..." + }, + { + "path": "docs_md/articles/abdominal-lymph-nodes_3ca98d42-20ef-48fe-9265-ebfe570ba54b.md", + "title": "Abdominal Lymph Nodes", + "docid": "3ca98d42-20ef-48fe-9265-ebfe570ba54b", + "breadcrumbs": [ + "Ultrasound", + "Anatomy", + "Abdomen", + "Abdominal Lymph Nodes" + ], + "authors": [ + { + "key": "07469ec4-05aa-4d65-a788-08b4d64048af", + "value": "Jade Wong-You-Cheong, MBChB, MRCP, FRCR, FSRU, FSAR" + } + ], + "pageKeywords": "Ultrasound, Anatomy, Abdomen, Abdominal Lymph Nodes", + "reasons": [ + "Content" + ], + "snippet": "# GROSS ANATOMY - ## Overview - Major lymphatic vessels and nodal chains lie along major blood vessels [aorta, inferior vena cav..." }, { "path": "docs_md/articles/temporal-bone-facial-nerve-venous-malformation-hemangioma_dcd6a44e-cbe6-457c-9b03-598a2b874ece.md", @@ -128,157 +193,158 @@ ], "pageKeywords": "Head and Neck, Diagnosis, Temporal Bone, Intratemporal Facial Nerve, Benign and Malignant Tumors, Temporal Bone Facial Nerve Venous Malformation (Hemangioma)", "reasons": [ - "Breadcrumbs", - "Keywords", - "Title" + "Content" ], - "snippet": "" + "snippet": "...* staining utilizing D2-40 antibody **negativity** excludes lymphatic malformation - ## Clinical Issues - Intratemporal FNVM produces **peri..." }, { - "path": "docs_md/articles/middle-ear-prolapsing-facial-nerve_61f6bcaa-f073-4385-b895-dc420ddc8a1e.md", - "title": "Middle Ear Prolapsing Facial Nerve", - "docid": "61f6bcaa-f073-4385-b895-dc420ddc8a1e", + "path": "docs_md/articles/colonic-wall-thickening_71ca9b30-6186-4129-957b-3b686b91b327.md", + "title": "Colonic Wall Thickening", + "docid": "71ca9b30-6186-4129-957b-3b686b91b327", "breadcrumbs": [ - "Head and Neck", - "Diagnosis", - "Temporal Bone", - "Intratemporal Facial Nerve", - "Pseudolesions", - "Middle Ear Prolapsing Facial Nerve" - ], - "authors": [ - { - "key": "94f835c8-fa13-4e8a-995b-53048e6b0605", - "value": "Philip R. Chapman, MD" - }, - { - "key": "33151213-01b2-4542-9105-342e006b3915", - "value": "H. Ric Harnsberger, MD" - } - ], - "pageKeywords": "Head and Neck, Diagnosis, Temporal Bone, Intratemporal Facial Nerve, Pseudolesions, Middle Ear Prolapsing Facial Nerve", - "reasons": [ - "Breadcrumbs", - "Keywords", - "Title" - ], - "snippet": "" - }, - { - "path": "docs_md/articles/facial-muscles-and-superficial-musculoaponeurotic-system_80a3629d-d41e-4efd-9dac-4f19bbdea9db.md", - "title": "Facial Muscles and Superficial Musculoaponeurotic System", - "docid": "80a3629d-d41e-4efd-9dac-4f19bbdea9db", - "breadcrumbs": [ - "Head and Neck", - "Anatomy", - "Face", - "Facial Muscles and Superficial Musculoaponeurotic System" - ], - "authors": [ - { - "key": "b2e6dabb-ee1c-42a4-a332-9f0814c1c607", - "value": "Surjith Vattoth, MD" - } - ], - "pageKeywords": "Head and Neck, Anatomy, Face, Facial Muscles and Superficial Musculoaponeurotic System", - "reasons": [ - "Breadcrumbs", - "Keywords", - "Title" - ], - "snippet": "" - }, - { - "path": "docs_md/articles/temporal-bone-facial-nerve-schwannoma_cf2bcc82-4a1b-4989-adeb-f4e82116111b.md", - "title": "Temporal Bone Facial Nerve Schwannoma", - "docid": "cf2bcc82-4a1b-4989-adeb-f4e82116111b", - "breadcrumbs": [ - "Head and Neck", - "Diagnosis", - "Temporal Bone", - "Intratemporal Facial Nerve", - "Benign and Malignant Tumors", - "Temporal Bone Facial Nerve Schwannoma" - ], - "authors": [ - { - "key": "07a2c087-6202-49e7-870b-7aa162d18f06", - "value": "Bronwyn E. Hamilton, MD" - }, - { - "key": "33151213-01b2-4542-9105-342e006b3915", - "value": "H. Ric Harnsberger, MD" - } - ], - "pageKeywords": "Head and Neck, Diagnosis, Temporal Bone, Intratemporal Facial Nerve, Benign and Malignant Tumors, Temporal Bone Facial Nerve Schwannoma", - "reasons": [ - "Breadcrumbs", - "Keywords", - "Title" - ], - "snippet": "" - }, - { - "path": "docs_md/articles/intratemporal-facial-nerve-enhancement_a3569ec5-a566-411d-877f-41ad832e3fd2.md", - "title": "Intratemporal Facial Nerve Enhancement", - "docid": "a3569ec5-a566-411d-877f-41ad832e3fd2", - "breadcrumbs": [ - "Head and Neck", - "Diagnosis", - "Temporal Bone", - "Intratemporal Facial Nerve", - "Pseudolesions", - "Intratemporal Facial Nerve Enhancement" - ], - "authors": [ - { - "key": "94f835c8-fa13-4e8a-995b-53048e6b0605", - "value": "Philip R. Chapman, MD" - } - ], - "pageKeywords": "Head and Neck, Diagnosis, Temporal Bone, Intratemporal Facial Nerve, Pseudolesions, Intratemporal Facial Nerve Enhancement", - "reasons": [ - "Breadcrumbs", - "Keywords", - "Title" - ], - "snippet": "" - }, - { - "path": "docs_md/articles/hemifacial-spasm_1b390143-1212-4447-beb3-ed9e85ef34e4.md", - "title": "Hemifacial Spasm", - "docid": "1b390143-1212-4447-beb3-ed9e85ef34e4", - "breadcrumbs": [ - "Head and Neck", + "Gastrointestinal", "Differential Diagnosis", - "CPA-IAC and Posterior Fossa", - "Clinically Based Differentials", - "Hemifacial Spasm" + "Colon", + "Generic Imaging Patterns", + "Colonic Wall Thickening" ], "authors": [ { - "key": "eef2f839-5706-47b9-89c3-60d8315b2b3a", - "value": "Nicholas A. Koontz, MD" + "key": "6c5a9e0e-9dea-461b-9ad4-c00f5c4c2bbf", + "value": "Atif Zaheer, MD, FSAR" + }, + { + "key": "e987d3d3-1206-48d6-824b-3347c2968855", + "value": "Michael P. Federle, MD, FACR" } ], - "pageKeywords": "Head and Neck, Differential Diagnosis, CPA-IAC and Posterior Fossa, Clinically Based Differentials, Hemifacial Spasm", + "pageKeywords": "Gastrointestinal, Differential Diagnosis, Colon, Generic Imaging Patterns, Colonic Wall Thickening", "reasons": [ - "Breadcrumbs", - "Keywords", - "Title" + "Content" ], - "snippet": "" + "snippet": "...n with regional adenopathy (strong predictor of cancer with lymphatic spread) - Look for hepatic and peritoneal metastases - **Portal Hy..." }, { - "path": "docs_md/articles/cpa-iac-facial-nerve-schwannoma_9db01630-23a4-4f42-ad83-0ec399503495.md", - "title": "CPA-IAC Facial Nerve Schwannoma", - "docid": "9db01630-23a4-4f42-ad83-0ec399503495", + "path": "docs_md/articles/lymphangioma-mesenteric-cyst_bb42a128-c819-4368-a085-232b6db3434c.md", + "title": "Lymphangioma (Mesenteric Cyst)", + "docid": "bb42a128-c819-4368-a085-232b6db3434c", + "breadcrumbs": [ + "Gastrointestinal", + "Diagnosis", + "Peritoneum, Mesentery, and Abdominal Wall", + "Benign Neoplasms", + "Lymphangioma (Mesenteric Cyst)" + ], + "authors": [ + { + "key": "c1df94ab-4a9f-44c4-add7-1f174fb9ac45", + "value": "Siva P. Raman, MD" + } + ], + "pageKeywords": "Gastrointestinal, Diagnosis, Peritoneum, Mesentery, and Abdominal Wall, Benign Neoplasms, Lymphangioma (Mesenteric Cyst)", + "reasons": [ + "Content" + ], + "snippet": "...ymphoepithelial cyst, cystic lymphangioma, mesenteric cyst, lymphatic malformation - ## Definitions - Generic descriptive term used for beni..." + }, + { + "path": "docs_md/articles/vertebral-body-t1-hypointense-signal-diffuse_5f4ceb8f-867f-4b4d-8149-57a906489453.md", + "title": "Vertebral Body, T1-Hypointense Signal, Diffuse", + "docid": "5f4ceb8f-867f-4b4d-8149-57a906489453", + "breadcrumbs": [ + "Spine", + "Differential Diagnosis", + "Vertebral Body, Posterior Elements", + "Modality-Specific Imaging Findings", + "Vertebral Body, T1-Hypointense Signal, Diffuse" + ], + "authors": [ + { + "key": "bee1f359-33fb-4cba-9e6b-ed1ca1842439", + "value": "Jeffrey S. Ross, MD" + }, + { + "key": "99e1aff7-f42c-43a0-95ae-d89c8551aa01", + "value": "Kevin R. Moore, MD" + } + ], + "pageKeywords": "Spine, Differential Diagnosis, Vertebral Body, Posterior Elements, Modality-Specific Imaging Findings, Vertebral Body, T1-Hypointense Signal, Diffuse", + "reasons": [ + "Content" + ], + "snippet": "...nation (arterial or venous via Batson plexus) > perineural, lymphatic, CSF spread - Marrow initially infiltrated, trabeculae destroy..." + }, + { + "path": "docs_md/articles/penetrating-atherosclerotic-ulcer_63f6cba2-2200-456a-8d03-ac3111e420c8.md", + "title": "Penetrating Atherosclerotic Ulcer", + "docid": "63f6cba2-2200-456a-8d03-ac3111e420c8", + "breadcrumbs": [ + "Cardiac", + "Diagnosis", + "Aorta", + "Penetrating Atherosclerotic Ulcer" + ], + "authors": [ + { + "key": "9ad9af12-61a1-44d6-af52-0ee1f38eb298", + "value": "Davis Vigneault, MD, DPhil" + }, + { + "key": "5de0df07-7b3e-4678-8767-1519e1153f29", + "value": "Dominik Fleischmann, MD" + }, + { + "key": "10bb95ac-a27a-4ebe-833b-e59fea07734b", + "value": "Santiago Martínez-Jiménez, MD, FACR" + } + ], + "pageKeywords": "Cardiac, Diagnosis, Aorta, Penetrating Atherosclerotic Ulcer", + "reasons": [ + "Content" + ], + "snippet": ".../ANATOMY:a7a252f0-2ac6-402a-8c87-cfce8adc799b ### Vessels, Lymphatic System, and Nerves, Abdominal Gastrointestinal/ANATOMY:0c38fd49-88e7-4272-960f-b..." + }, + { + "path": "docs_md/articles/mycotic-aneurysm_20616d0a-7e0b-48d9-9be9-1af29e3dd6da.md", + "title": "Mycotic Aneurysm", + "docid": "20616d0a-7e0b-48d9-9be9-1af29e3dd6da", + "breadcrumbs": [ + "Cardiac", + "Diagnosis", + "Aorta", + "Mycotic Aneurysm" + ], + "authors": [ + { + "key": "ee6ece9d-ad74-458c-a8df-11628ae7f879", + "value": "Arzu Canan, MD" + }, + { + "key": "3d1e4c57-c1cf-4c89-b0f0-5d82b29a31e1", + "value": "Suhny Abbara, MD, FACR, MSCCT, FNASCI" + }, + { + "key": "501b57b9-1723-4b1e-b21e-d4516655fac8", + "value": "Sanjeeva P. Kalva, MD, FSIR, FCIRSE" + } + ], + "pageKeywords": "Cardiac, Diagnosis, Aorta, Mycotic Aneurysm", + "reasons": [ + "Content" + ], + "snippet": "...nal infection or intraabdominal abscess - Lymphatic spread - *Burkholderia pseudomallei* (causing melioidosis), endemic in..." + }, + { + "path": "docs_md/articles/otosclerosis_ddc7b884-3c17-4834-9e96-d985c6b618a9.md", + "title": "Otosclerosis", + "docid": "ddc7b884-3c17-4834-9e96-d985c6b618a9", "breadcrumbs": [ "Head and Neck", "Diagnosis", - "CPA-IAC", - "Benign and Malignant Tumors", - "CPA-IAC Facial Nerve Schwannoma" + "Temporal Bone", + "Inner Ear", + "Infectious and Inflammatory Lesions", + "Otosclerosis" ], "authors": [ { @@ -286,41 +352,232 @@ "value": "Bronwyn E. Hamilton, MD" }, { - "key": "33151213-01b2-4542-9105-342e006b3915", - "value": "H. Ric Harnsberger, MD" + "key": "4b6589b0-9b8d-4467-8a90-01a0a59742fc", + "value": "Troy A. Hutchins, MD" } ], - "pageKeywords": "Head and Neck, Diagnosis, CPA-IAC, Benign and Malignant Tumors, CPA-IAC Facial Nerve Schwannoma", + "pageKeywords": "Head and Neck, Diagnosis, Temporal Bone, Inner Ear, Infectious and Inflammatory Lesions, Otosclerosis", "reasons": [ - "Breadcrumbs", - "Keywords", - "Title" + "Content" ], - "snippet": "" + "snippet": "...kaida T et al: Magnetic resonance imaging evaluation of endolymphatic hydrops in cases with otosclerosis. Otol Neurotol. 36(7):1146-50, 2015](http://w..." }, { - "path": "docs_md/articles/facial-nerve-lesion-temporal-bone_1428754b-a8ee-48a0-98f8-4faeebf8dbab.md", - "title": "Facial Nerve Lesion, Temporal Bone", - "docid": "1428754b-a8ee-48a0-98f8-4faeebf8dbab", + "path": "docs_md/articles/adrenal-cyst_c5d717a3-3d6e-4e86-9efe-1ad0ec14740f.md", + "title": "Adrenal Cyst", + "docid": "c5d717a3-3d6e-4e86-9efe-1ad0ec14740f", + "breadcrumbs": [ + "Genitourinary", + "Diagnosis", + "Adrenal", + "Benign Neoplasms", + "Adrenal Cyst" + ], + "authors": [ + { + "key": "c3463c5c-31d3-4489-bbfe-6b895abdb86d", + "value": "Mitchell Tublin, MD" + } + ], + "pageKeywords": "Genitourinary, Diagnosis, Adrenal, Benign Neoplasms, Adrenal Cyst", + "reasons": [ + "Content" + ], + "snippet": "...om preexisting vascular malformation or obstructed, ectatic lymphatic channels - Thin rim calcification typical - Epitheli..." + }, + { + "path": "docs_md/articles/hydrocephalus_e9481739-278e-4682-ab1e-4326a77c3d0c.md", + "title": "Hydrocephalus", + "docid": "e9481739-278e-4682-ab1e-4326a77c3d0c", + "breadcrumbs": [ + "Pediatrics", + "Diagnosis", + "Pediatric Neuroradiology", + "Brain", + "Anatomy-Based Diagnoses", + "Ventricles and Cisterns", + "Hydrocephalus" + ], + "authors": [ + { + "key": "838e1722-2479-4fbd-a5fe-d965980a1a2c", + "value": "Blaise V. Jones, MD" + } + ], + "pageKeywords": "Pediatrics, Diagnosis, Pediatric Neuroradiology, Brain, Anatomy-Based Diagnoses, Ventricles and Cisterns, Hydrocephalus", + "reasons": [ + "Content" + ], + "snippet": "...Extracellular, extravascular fluid in brain is managed by glymphatic system - CSF circulates from within ventricles → subarachnoid space →..." + }, + { + "path": "docs_md/articles/inner-ear-lesion-adult_e5bbf757-d77a-4546-a848-d1a1a64cb230.md", + "title": "Inner Ear Lesion, Adult", + "docid": "e5bbf757-d77a-4546-a848-d1a1a64cb230", "breadcrumbs": [ "Head and Neck", "Differential Diagnosis", "Temporal Bone", "Anatomically Based Differentials", - "Facial Nerve Lesion, Temporal Bone" + "Inner Ear Lesion, Adult" ], "authors": [ { - "key": "eef2f839-5706-47b9-89c3-60d8315b2b3a", - "value": "Nicholas A. Koontz, MD" + "key": "d19354f3-7ff2-495a-ad3f-064122e45602", + "value": "Bernadette L. Koch, MD" } ], - "pageKeywords": "Head and Neck, Differential Diagnosis, Temporal Bone, Anatomically Based Differentials, Facial Nerve Lesion, Temporal Bone", + "pageKeywords": "Head and Neck, Differential Diagnosis, Temporal Bone, Anatomically Based Differentials, Inner Ear Lesion, Adult", "reasons": [ - "Breadcrumbs", - "Keywords", - "Title" + "Content" ], - "snippet": "" + "snippet": "...ochlear turns - Usually with enlarged endolymphatic sac/duct - Literature suggests dysplastic osseous spiral lamin..." + }, + { + "path": "docs_md/articles/ventricles-and-cisterns-overview_ad860c4f-fe9a-4469-8eca-a7ccd5cff70f.md", + "title": "Ventricles and Cisterns Overview", + "docid": "ad860c4f-fe9a-4469-8eca-a7ccd5cff70f", + "breadcrumbs": [ + "Brain", + "Diagnosis", + "Anatomy-Based Diagnoses", + "Ventricles and Cisterns", + "Ventricles and Cisterns Overview" + ], + "authors": [ + { + "key": "a25c450b-3d34-4f64-bba3-cc0834813df6", + "value": "Miral D. Jhaveri, MD, MBA" + } + ], + "pageKeywords": "Brain, Diagnosis, Anatomy-Based Diagnoses, Ventricles and Cisterns, Ventricles and Cisterns Overview", + "reasons": [ + "Content" + ], + "snippet": "...lli along the superior sagittal sinus. CSF also drains into lymphatic vessels around the cranial cavity and spinal canal. - **Updated model of CSF and..." + }, + { + "path": "docs_md/articles/takayasu-arteritis_d35eb6f3-bfd3-4121-8781-325a93ccc197.md", + "title": "Takayasu Arteritis", + "docid": "d35eb6f3-bfd3-4121-8781-325a93ccc197", + "breadcrumbs": [ + "Cardiac", + "Diagnosis", + "Aorta", + "Takayasu Arteritis" + ], + "authors": [ + { + "key": "ee6ece9d-ad74-458c-a8df-11628ae7f879", + "value": "Arzu Canan, MD" + }, + { + "key": "3d1e4c57-c1cf-4c89-b0f0-5d82b29a31e1", + "value": "Suhny Abbara, MD, FACR, MSCCT, FNASCI" + } + ], + "pageKeywords": "Cardiac, Diagnosis, Aorta, Takayasu Arteritis", + "reasons": [ + "Content" + ], + "snippet": ".../ANATOMY:551f4b47-fac1-44f9-b800-09ce766fedd9 ### Vessels, Lymphatic System, and Nerves, Abdominal Gastrointestinal/ANATOMY:0c38fd49-88e7-4272-960f-b..." + }, + { + "path": "docs_md/articles/peritoneal-spaces-and-structures_9c50ad6a-e96b-44a1-93d1-a4e7de5212c2.md", + "title": "Peritoneal Spaces and Structures", + "docid": "9c50ad6a-e96b-44a1-93d1-a4e7de5212c2", + "breadcrumbs": [ + "Ultrasound", + "Anatomy", + "Abdomen", + "Peritoneal Spaces and Structures" + ], + "authors": [ + { + "key": "07469ec4-05aa-4d65-a788-08b4d64048af", + "value": "Jade Wong-You-Cheong, MBChB, MRCP, FRCR, FSRU, FSAR" + } + ], + "pageKeywords": "Ultrasound, Anatomy, Abdomen, Peritoneal Spaces and Structures", + "reasons": [ + "Content" + ], + "snippet": "...to right; contains superior mesenteric vessels, nerves, and lymphatics - Transverse mesocolon crosses almost horizontally in front of pancreas, du..." + }, + { + "path": "docs_md/articles/cpa-iac-lipoma_37ef2fb6-7d7e-48dc-b786-16ed4475b26e.md", + "title": "CPA-IAC Lipoma", + "docid": "37ef2fb6-7d7e-48dc-b786-16ed4475b26e", + "breadcrumbs": [ + "Head and Neck", + "Diagnosis", + "CPA-IAC", + "Congenital Lesions", + "CPA-IAC Lipoma" + ], + "authors": [ + { + "key": "318f80ab-6abb-4067-a809-2ebdaa5a30c9", + "value": "Kalen Riley, MD, MBA" + }, + { + "key": "e0282a62-994d-4550-a127-1eb773b1e920", + "value": "Blair A. Winegar, MD" + }, + { + "key": "94f835c8-fa13-4e8a-995b-53048e6b0605", + "value": "Philip R. Chapman, MD" + } + ], + "pageKeywords": "Head and Neck, Diagnosis, CPA-IAC, Congenital Lesions, CPA-IAC Lipoma", + "reasons": [ + "Content" + ], + "snippet": "...ant CPA-IAC lipoma with inner ear second fatty focus in endolymphatic duct-sac.\\n\\nThree axial CT (#1-3) and 3 axial MR (#4-6) images reveal an obviou..." + }, + { + "path": "docs_md/articles/cystic-cpa-mass_6c60db6d-8093-4df5-8cbb-c6f6570ae167.md", + "title": "Cystic CPA Mass", + "docid": "6c60db6d-8093-4df5-8cbb-c6f6570ae167", + "breadcrumbs": [ + "Head and Neck", + "Differential Diagnosis", + "CPA-IAC and Posterior Fossa", + "Generic Imaging Patterns", + "Cystic CPA Mass" + ], + "authors": [ + { + "key": "07a2c087-6202-49e7-870b-7aa162d18f06", + "value": "Bronwyn E. Hamilton, MD" + } + ], + "pageKeywords": "Head and Neck, Differential Diagnosis, CPA-IAC and Posterior Fossa, Generic Imaging Patterns, Cystic CPA Mass", + "reasons": [ + "Content" + ], + "snippet": "...chnoid, & neurenteric cysts; neurocysticercosis & large endolymphatic sac anomaly - Many solid CPA tumors may have either intramural cysts,..." + }, + { + "path": "docs_md/articles/cranial-meninges_3210cf7d-5be6-4741-a11d-dd7f8c1bc286.md", + "title": "Cranial Meninges", + "docid": "3210cf7d-5be6-4741-a11d-dd7f8c1bc286", + "breadcrumbs": [ + "Brain", + "Anatomy", + "Scalp, Skull, and Meninges", + "Cranial Meninges" + ], + "authors": [ + { + "key": "5cff4116-3654-4b3a-bb75-5ebe0b8c9850", + "value": "Anne G. Osborn, MD, FACR" + } + ], + "pageKeywords": "Brain, Anatomy, Scalp, Skull, and Meninges, Cranial Meninges", + "reasons": [ + "Content" + ], + "snippet": "...along penetrating arteries - Key part of brain \"glymphatic\" system ## GROSS ANATOMY - ### Overview - Brain encased by 3 meninge..." } ] \ No newline at end of file diff --git a/tools/search_md.py b/tools/search_md.py index 0599471..2630bc6 100644 --- a/tools/search_md.py +++ b/tools/search_md.py @@ -249,11 +249,33 @@ def match_author(authors: List[Dict[str, Any]], q: str) -> bool: return False -def run_search(root: str, qkey: str, qval: str, mode: str = 'exact', targets: List[str] = None) -> List[Dict[str, Any]]: +def run_search(root: str, qkey: str, qval: str, mode: str = 'exact', targets: List[str] = None, or_queries: List[Dict[str, Any]] = None) -> List[Dict[str, Any]]: out = [] - # Helper to test if a string/list/dict matches under the chosen mode - def value_matches(query: str, target_val: Any) -> bool: + # Normalize queries list + if not or_queries: + or_queries = [{ + 'qkey': qkey, + 'qval': qval, + 'mode': mode, + 'targets': targets + }] + + normalized_specs = [] + for spec in or_queries: + qk = spec.get('qkey') or 'global' + qv = spec.get('qval') or '' + m = spec.get('mode') or 'exact' + t = spec.get('targets') + normalized_specs.append({ + 'qkey': qk, + 'qval': qv, + 'mode': m, + 'targets': t + }) + + # Helper to test if a string/list/dict matches under a specific mode + def value_matches(query: str, target_val: Any, mode_val: str) -> bool: if target_val is None: return False @@ -261,15 +283,15 @@ def run_search(root: str, qkey: str, qval: str, mode: str = 'exact', targets: Li for item in target_val: if isinstance(item, dict): for k, v in item.items(): - if value_matches(query, v): + if value_matches(query, v, mode_val): return True else: - if value_matches(query, item): + if value_matches(query, item, mode_val): return True return False elif isinstance(target_val, dict): for k, v in target_val.items(): - if value_matches(query, v): + if value_matches(query, v, mode_val): return True return False @@ -277,9 +299,9 @@ def run_search(root: str, qkey: str, qval: str, mode: str = 'exact', targets: Li if not s_val: return False - if mode == 'stemming': + if mode_val == 'stemming': return match_stemming(query, s_val) - elif mode == 'fuzzy': + elif mode_val == 'fuzzy': return match_fuzzy(query, s_val) else: # Exact substring or wildcard match @@ -303,108 +325,126 @@ def run_search(root: str, qkey: str, qval: str, mode: str = 'exact', targets: Li reasons = [] snippet = "" - # Normalise query key. If none or global/all/empty, search all - k_lower = (qkey or '').strip().lower() - if not k_lower or k_lower in ('global', 'all', 'any'): - # Determine which targets to test - if targets: - target_set = {t.lower() for t in targets} - else: - target_set = {'title', 'docid', 'breadcrumbs', 'authors', 'keywords', 'category', 'type', 'content'} + for spec in normalized_specs: + qk = spec['qkey'] + qv = spec['qval'] + m = spec['mode'] + t = spec['targets'] - # Single search box mode / global search: - # 1. Title - if 'title' in target_set: - for f in ['title', 'enhancedTitle', 'pageTitle']: - if value_matches(qval, fm.get(f)): - matched = True - reasons.append('Title') - break - # 2. DocID - if 'docid' in target_set: - if value_matches(qval, fm.get('docid')): - matched = True - reasons.append('DocID') - # 3. Breadcrumbs - if 'breadcrumbs' in target_set: - bcs = fm.get('breadcrumbs') or [] - if isinstance(bcs, list) and bcs and isinstance(bcs[0], dict): - bcs_list = [d.get('name') or d.get('slug') or '' for d in bcs] + if not qv: + continue # skip empty queries + + spec_matched = False + spec_reasons = [] + spec_snippet = "" + + k_lower = (qk or '').strip().lower() + if not k_lower or k_lower in ('global', 'all', 'any'): + # Determine which targets to test + if t: + target_set = {x.lower() for x in t} else: - bcs_list = bcs - if '>' in qval: - query_parts = [p.strip() for p in qval.split('>') if p.strip()] - if match_breadcrumbs(bcs_list, query_parts): - matched = True - reasons.append('Breadcrumbs') - else: - if value_matches(qval, bcs_list): - matched = True - reasons.append('Breadcrumbs') - # 4. Authors - if 'authors' in target_set: - if value_matches(qval, fm.get('authors')): - matched = True - reasons.append('Authors') - # 5. PageKeywords - if 'keywords' in target_set or 'pagekeywords' in target_set: - if value_matches(qval, fm.get('pageKeywords')): - matched = True - reasons.append('Keywords') - # 6. Category - if 'category' in target_set: - if value_matches(qval, fm.get('category')): - matched = True - reasons.append('Category') - # 7. Type - if 'type' in target_set: - if value_matches(qval, fm.get('type')): - matched = True - reasons.append('Type') - # 8. Content - if 'content' in target_set: - if value_matches(qval, content): - matched = True - reasons.append('Content') - snippet = get_content_snippet(content, qval) - else: - # Specific key search - if k_lower == 'breadcrumbs': - bcs = fm.get('breadcrumbs') or [] - if bcs and isinstance(bcs, list) and bcs and isinstance(bcs[0], dict): - bcs_list = [d.get('name') or d.get('slug') or '' for d in bcs] - else: - bcs_list = bcs - if '>' in qval: - query_parts = [p.strip() for p in qval.split('>') if p.strip()] - matched = match_breadcrumbs(bcs_list, query_parts) - else: - matched = value_matches(qval, bcs_list) - if matched: - reasons.append('Breadcrumbs') - elif k_lower == 'authors': - matched = value_matches(qval, fm.get('authors')) - if matched: - reasons.append('Authors') - elif k_lower == 'pagekeywords': - matched = value_matches(qval, fm.get('pageKeywords')) - if matched: - reasons.append('Keywords') - elif k_lower == 'content': - matched = value_matches(qval, content) - if matched: - reasons.append('Content') - snippet = get_content_snippet(content, qval) - elif k_lower in ('title', 'enhancedtitle', 'pagetitle'): - for f in ['title', 'enhancedTitle', 'pageTitle']: - if value_matches(qval, fm.get(f)): - matched = True - reasons.append('Title') - break + target_set = {'title', 'docid', 'breadcrumbs', 'authors', 'keywords', 'category', 'type', 'content'} + + # 1. Title + if 'title' in target_set: + for f in ['title', 'enhancedTitle', 'pageTitle']: + if value_matches(qv, fm.get(f), m): + spec_matched = True + spec_reasons.append('Title') + break + # 2. DocID + if 'docid' in target_set: + if value_matches(qv, fm.get('docid'), m): + spec_matched = True + spec_reasons.append('DocID') + # 3. Breadcrumbs + if 'breadcrumbs' in target_set: + bcs = fm.get('breadcrumbs') or [] + if isinstance(bcs, list) and bcs and isinstance(bcs[0], dict): + bcs_list = [d.get('name') or d.get('slug') or '' for d in bcs] + else: + bcs_list = bcs + if '>' in qv: + query_parts = [p.strip() for p in qv.split('>') if p.strip()] + if match_breadcrumbs(bcs_list, query_parts): + spec_matched = True + spec_reasons.append('Breadcrumbs') + else: + if value_matches(qv, bcs_list, m): + spec_matched = True + spec_reasons.append('Breadcrumbs') + # 4. Authors + if 'authors' in target_set: + if value_matches(qv, fm.get('authors'), m): + spec_matched = True + spec_reasons.append('Authors') + # 5. PageKeywords + if 'keywords' in target_set or 'pagekeywords' in target_set: + if value_matches(qv, fm.get('pageKeywords'), m): + spec_matched = True + spec_reasons.append('Keywords') + # 6. Category + if 'category' in target_set: + if value_matches(qv, fm.get('category'), m): + spec_matched = True + spec_reasons.append('Category') + # 7. Type + if 'type' in target_set: + if value_matches(qv, fm.get('type'), m): + spec_matched = True + spec_reasons.append('Type') + # 8. Content + if 'content' in target_set: + if value_matches(qv, content, m): + spec_matched = True + spec_reasons.append('Content') + spec_snippet = get_content_snippet(content, qv) else: - matched = value_matches(qval, fm.get(qkey)) - if matched: - reasons.append(qkey.capitalize()) + # Specific key search + if k_lower == 'breadcrumbs': + bcs = fm.get('breadcrumbs') or [] + if bcs and isinstance(bcs, list) and bcs and isinstance(bcs[0], dict): + bcs_list = [d.get('name') or d.get('slug') or '' for d in bcs] + else: + bcs_list = bcs + if '>' in qv: + query_parts = [p.strip() for p in qv.split('>') if p.strip()] + spec_matched = match_breadcrumbs(bcs_list, query_parts) + else: + spec_matched = value_matches(qv, bcs_list, m) + if spec_matched: + spec_reasons.append('Breadcrumbs') + elif k_lower == 'authors': + spec_matched = value_matches(qv, fm.get('authors'), m) + if spec_matched: + spec_reasons.append('Authors') + elif k_lower == 'pagekeywords': + spec_matched = value_matches(qv, fm.get('pageKeywords'), m) + if spec_matched: + spec_reasons.append('Keywords') + elif k_lower == 'content': + spec_matched = value_matches(qv, content, m) + if spec_matched: + spec_reasons.append('Content') + spec_snippet = get_content_snippet(content, qv) + elif k_lower in ('title', 'enhancedtitle', 'pagetitle'): + for f in ['title', 'enhancedTitle', 'pageTitle']: + if value_matches(qv, fm.get(f), m): + spec_matched = True + spec_reasons.append('Title') + break + else: + spec_matched = value_matches(qv, fm.get(qk), m) + if spec_matched: + spec_reasons.append(qk.capitalize()) + + if spec_matched: + matched = True + spec_reasons_clean = [str(x) for x in spec_reasons] + reasons.extend(spec_reasons_clean) + if spec_snippet and not snippet: + snippet = spec_snippet if matched: out.append({ diff --git a/tools/search_md_gui.py b/tools/search_md_gui.py index 8469ccb..7f4eb55 100644 --- a/tools/search_md_gui.py +++ b/tools/search_md_gui.py @@ -169,41 +169,82 @@ def search_page() -> None: # build UI with ui.row().classes('gap-2'): ui.button('Open Output Dir', icon='folder', on_click=lambda: open_output_dir(), color='secondary').props('outline') - # Main Search Box Card - with ui.card().classes('w-full p-6 shadow-xl'): - with ui.row().classes('w-full items-center gap-4'): - # Single search box - qval_input = ui.input( - label='Search Query', - placeholder='Type search terms...' - ).classes('flex-grow').props('outlined label-slot') - qval_input.add_slot('append', '') - - # Match mode select - mode_select = ui.select( - options={'exact': 'Exact Match', 'fuzzy': 'Fuzzy Match', 'stemming': 'Stemming (Related Words)'}, - value='exact', - label='Match Mode' - ).classes('w-56').props('outlined') - - # Targets select - targets_select = ui.select( - options={ - 'Title': 'Title', - 'Content': 'Content', - 'Breadcrumbs': 'Breadcrumbs', - 'Authors': 'Authors', - 'Keywords': 'Keywords', - 'Category': 'Category', - 'Type': 'Type' - }, - value=['Title', 'Content', 'Breadcrumbs', 'Keywords', 'Category', 'Type'], - label='Search Targets', - multiple=True - ).classes('w-80').props('outlined use-chips') - - # Search button - search_btn = ui.button('Search', icon='search', on_click=lambda: asyncio.create_task(run_search_handler())).classes('bg-blue-600 hover:bg-blue-700 text-white font-semibold py-4 px-8 rounded-lg shadow-md transition-all duration-300') + # Main Search card + queries = [ + { + 'qval': '', + 'mode': 'exact', + 'targets': ['Title', 'Content', 'Breadcrumbs', 'Keywords', 'Category', 'Type'] + } + ] + + with ui.card().classes('w-full p-6 shadow-xl gap-4'): + query_blocks_container = ui.column().classes('w-full gap-4') + + def render_query_blocks(): + query_blocks_container.clear() + with query_blocks_container: + for idx, q in enumerate(queries): + with ui.row().classes('w-full items-center gap-4 border border-slate-800 p-4 rounded-lg bg-slate-900/40 relative'): + label_text = "Query 1 (Primary)" if idx == 0 else f"OR Query {idx + 1}" + ui.label(label_text).classes('text-blue-400 font-semibold w-36') + + # Bind value and update on change + qval_input = ui.input( + label='Search Query', + placeholder='Type search terms...', + value=q['qval'], + on_change=lambda e, q=q: q.update({'qval': e.value}) + ).classes('flex-grow').props('outlined dense').on('keydown.enter', lambda: asyncio.create_task(run_search_handler())) + + mode_select = ui.select( + options={'exact': 'Exact Match', 'fuzzy': 'Fuzzy Match', 'stemming': 'Stemming (Related Words)'}, + value=q['mode'], + label='Match Mode', + on_change=lambda e, q=q: q.update({'mode': e.value}) + ).classes('w-48').props('outlined dense') + + targets_select = ui.select( + options={ + 'Title': 'Title', + 'Content': 'Content', + 'Breadcrumbs': 'Breadcrumbs', + 'Authors': 'Authors', + 'Keywords': 'Keywords', + 'Category': 'Category', + 'Type': 'Type' + }, + value=q['targets'], + label='Search Targets', + multiple=True, + on_change=lambda e, q=q: q.update({'targets': e.value}) + ).classes('w-72').props('outlined dense use-chips') + + if idx > 0: + ui.button( + icon='delete', + on_click=lambda _, idx=idx: remove_query_block(idx) + ).classes('text-red-400 hover:text-red-500').props('flat round dense') + + def add_query_block(): + queries.append({ + 'qval': '', + 'mode': 'exact', + 'targets': ['Title', 'Content', 'Breadcrumbs', 'Keywords', 'Category', 'Type'] + }) + render_query_blocks() + + def remove_query_block(idx: int): + if 0 < idx < len(queries): + queries.pop(idx) + render_query_blocks() + + # Render initial query blocks + render_query_blocks() + + with ui.row().classes('w-full justify-between items-center gap-4 mt-2'): + ui.button('Add OR Query', icon='add', on_click=add_query_block).classes('text-blue-400 border border-blue-800').props('outline dense') + search_btn = ui.button('Search', icon='search', on_click=lambda: asyncio.create_task(run_search_handler())).classes('bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-8 rounded-lg shadow-md transition-all duration-300') # Advanced Criteria Expansion Box with ui.expansion('Advanced Configurations', icon='settings').classes('w-full text-slate-300 border border-slate-800 rounded-lg p-2'): @@ -283,7 +324,12 @@ def search_page() -> None: # build UI ''') async def run_search_handler(): - if not qval_input.value: + has_query = False + for q in queries: + if q['qval'].strip(): + has_query = True + break + if not has_query: ui.notify('Please enter a search query', color='warning') return @@ -291,13 +337,24 @@ def search_page() -> None: # build UI search_btn.set_enabled(False) try: + or_queries_param = [] + for q in queries: + if q['qval'].strip(): + or_queries_param.append({ + 'qkey': key_select.value, + 'qval': q['qval'], + 'mode': q['mode'], + 'targets': [t.lower() for t in q['targets']] + }) + results = await anyio.to_thread.run_sync( search_md.run_search, root_input.value, - key_select.value, - qval_input.value, - mode_select.value, - targets_select.value + 'global', + '', + 'exact', + None, + or_queries_param ) table.rows = results