การค้นหา SNOMED-CT Disorder เพื่อลงวินิจฉัยด้วยวิธี Full-text Search

Sirawat
HLab
Published in
9 min readApr 5, 2023

Introduction

SNOMED-CT เป็นคลังคำศัพท์ทางการแพทย์มาตราฐานขนาดใหญ่ที่สามารถทำให้ข้อมูลใน Electronic health record สามารถสื่อสารความหมายของข้อมูลระหว่างโรงพยาบาลได้ตรงกัน หรือกระทั่งสามารถนำไปวิเคราะห์ข้อมูลได้อย่างสะดวก

ในบทความนี้จะนำ SNOMED-CT ส่วนของ Clinical Finding และ Disorder มาทำฟอร์มกรอก Diagnosis โดยใช้เทคนิคการค้นหาแบบ Full-text search ด้วย SQL server และ RediSearch

เนื้อหามีหลายประเด็น ทั้งสายการแพทย์และโปรแกรมมิ่ง

  • SNOMED-CT: concept, disorder, is-a relation, definition status
  • SNOMED-CT on SQL and RediSearch (+Elasticsearch opinion)
  • Front-end & Back-end technique for searching

จากการที่ได้ทดลอง implement การค้นหา Diagnosis/ Disorder ใน SNOMED-CT ด้วยฐานข้อมูล SQL server, RediSearch และ Elastic Enterprise Search พบว่า RediSearch เป็นฐานข้อมูลที่ทำให้การค้นรวดเร็ว ปรับแต่งให้การค้นหาตรงกับความต้องการผู้ใช้งานได้สะดวก และง่ายต่อการดูแลระบบ

พื้นฐานเรื่อง SNOMED-CT

ผู้อ่านที่มาจากสายแพทย์ที่ยังไม่มีพื้นฐานเรื่อง SNOMED-CT อาจจะลองศึกษาเบื้องต้นได้ก่อน

ปัจจุบันประเทศไทยเป็นสมาชิก SNOMED-CT แล้วตั้งแต่ กุมภาพันธ์ 2022 ทำให้โรงพยาบาลในไทยสามารถเอาฐานข้อมูลมาใช้ประโยชน์ได้แบบฟรี ๆ

https://www.snomed.org/our-stakeholders/member/thailand

Top-level concept

Root level ที่เราจะใช้ในบทความนี้คือ Clinical finding >> Disorder

SNOMED-CT มี concept ที่หลากหลาย, ภาพจากคู่มือ SNOMED CT ภาษาไทย โดย สมทส

ทำไมต้องเริ่มจาก Diagnosis (Disorder)

  • แพทย์ใช้คำภาษาอังกฤษเป็นหลัก ไม่ต้องแปลภาษาก่อนนำคำมาใช้ ไม่ต้องใช้ท่ายากถึง NLP (natural language processing) อย่างใน SNOMED-Finding
  • เมื่อผู้ใช้เลือกคำจากการค้นหา จะได้ SNOMED concept ที่แปลงเป็นรหัส SCTID/ Description ID ได้ในขั้นตอนเดียว การเตรียมข้อมูล (อาจจะ)ไม่ต้อง denormalization ก่อนเอาไปทำ Full-text search database
  • รหัส SCTID มีความสัมพันธ์กันแบบกราฟ สามารถแปลงไปหาข้อมูลอื่นๆ ได้ เช่น ตัวอย่างโรค COPD (chronic obstructive airways disease) มี parent, child ได้ ตามรูปตัวอย่าง
  • เมื่อแพทย์ลง Disorder ที่หลังบ้านเช็คเจอว่ามี children สามารถไปใช้แนะนำแพทย์ เพื่อให้การลงวินิจฉัยละเอียดเท่าที่จะทำได้ หรือนำ parent ไปใช้จัดกลุ่มโรคก็ได้
  • SNOMED-CT สามารถแปลงเป็น ICD-10 ได้ สามารถทำให้ experience การลงข้อมูลของแพทย์ทำได้สะดวกขึ้น ในขณะเดียวกัน Coder สามารถทำข้อมูลส่งเบิกได้สะดวกขึ้น (ฐานข้อมูลการแปลง SNOMED-CT to ICD-10 สามารถค้นหาได้จาก THIS website เช่นกัน มีทั้งแบบ many-to-one, many-to-many และอื่นๆ)
SNOMED-CT เป็นข้อมูลที่กว้างใหญ่ครอบคลุมหลายหัวข้อ เทียบกับ ICD-10 ที่เล็กนิดเดียว

พื้นฐานของ SNOMED-CT structure ที่เกี่ยวข้อง

Concept

Concept คือหน่วยย่อยที่เล็กที่สุดของ SNOMED-CT ในกรณีนี้ถือเป็น 1 แถวที่เก็บใน database (อ่านเพิ่มเติมได้จาก Blog: ทำไมเราจึงควรใช้ Clinical Terminology (เช่น SNOMED CT)

แนะนำดู SNOMED-CT Logical model https://confluence.ihtsdotools.org/display/DOCSTART/5.+SNOMED+CT+Logical+Model จะมี concept อยู่ตรงกลาง มีกลุ่มของ Description ที่เป็นแบบ Fully Specified Name และ Synonym ส่วนของ Relationship จะมีแบบ Is-a และ Attribute relationship

Is-a relationship

Is-a relationship คือความสัมพันธ์แบบ Hierarchy ที่ลักษณะเป็นแบบ Graph database ในฐานข้อมูลจะมี relationship table แยกออกมา ทำให้สามารถค้นหา hierarchy ได้ทั้งทาง source และทาง destination

ยกตัวอย่าง concept ที่สนใจคือ 127287001 |Intertrochanteric fracture|

  • ค้นหา Is-a ไปทาง destination จะได้ 263225007 |Fracture of proximal end of femur|
  • ค้นหา Is-a ไปทาง source จะได้ 26938002 |Open intertrochanteric fracture| และ 89820008 |Closed intertrochanteric fracture|

จะเห็นว่าการบันทึกวินิจฉัยด้วย SNOMED-CT จะสามารถค้นหาความสัมพันธ์ได้ทั้งทางกว้างขึ้น เช่นต้องการสถิติกลุ่มโรค Femur fracture และแบบจำเพาะเจาะจงมากขึ้น เช่น ต้องการระบบที่ช่วยแนะนำแพทย์ให้ลงวินิจฉัยให้ละเอียดขึ้น

ตัวอย่างบางส่วนของ SNOMED relation table แสดงความสัมพันธ์ของ concept ไปทาง source และ destination

รู้จักกับ Definition status (core metadata concept): Primitive concept vs Defined concept

สำหรับ disorder ที่จะใช้ในขั้นตอนต่อไป จะแบ่งเป็น Primitive: Not sufficiently defined by necessary conditions definition status กับ Defined: Sufficiently defined by necessary conditions definition status ซึ่งจะทำให้ระบบเลือกใช้หรือใช้ทั้งคู่โดยจัดลำดับคำแนะนำได้ ตัวอย่างเช่น

  • การใช้ในห้องฉุกเฉิน ซึ่งอาจจะยังมีข้อมูลไม่มากพอที่จะสรุปผลโรควินิจฉัยแบบ defined ข้อมูลทั้ง primitive และ defined อาจจะให้ความสำคัญเท่ากัน ตัวอย่างของ primitive เช่น Hypertensive urgency (primitive)
  • การใช้ในผู้ป่วยใน การลง discharge summary จะมีข้อมูลมากพอ สามารถแนะนำให้ลำดับของ Defined มาก่อน Primitive concept ได้
Definition status แบ่งเป็น primitive และ defined

เตรียมข้อมูล

Download resource

Download ได้จาก https://www.this.or.th/ หรือ https://www.this.or.th/service/snomed-ct/snomed-dl/ เลือกเป็นตัว snapshot

ได้ไฟล์มาแล้ว แตกไฟล์เจอไฟล์ที่เราจะเอามาใช้ เป็น Disorder.xlsx

ได้ disorder ที่จะเอาเข้า database แล้ว, 120437 records

ก่อนจะเอาเข้า SQL database แวะอ่าน SNOMED CT — SQL Practical Guide ที่แนะนำ SQL data type, วิธีการ indexing และตัวอย่างการใช้ full-text search

SCTID เก็บเป็น bigint (8 byte),​ หรือจะเก็บเป็น varchar ก็น่าจะดีกว่าเพราะในโค้ดเอามา identifier concat กัน ตามรูปด้านล่าง
SCTID ควรเก็บเป็น varchar มากกว่า (ภาพจากคู่มือ SNOMED-CT ภาษาไทย)
ตัวอย่างคำแนะนำการทำ index/ full-text index (SQL Practical Guide)

Import into SQL server database

สามารถใช้วิธีไหนก็ได้แล้วแต่สะดวก ในตัวอย่างนี้จะเป็นเอา SNOMED term ทุกอย่างเข้า database ไปไว้ตาราง description ก่อน

เอาข้อมูลเข้าตารางแบบตรงๆ

Preview SNOMED-CT Disorder

SNOMED Disorder จะมีประเภทของ definition status แบ่งเป็น primitive และ defined ให้เห็นใน Database

หาว่าเป็น concept ประเภท Primitive หรือ Defined หาจากตาราง concept

ลอง SELECT ด้วย search term LIKE “hypertensi%” โดยเอาเฉพาะ primitive concept และลองหาแบบเอาเฉพาะ defined concept

Disorder ที่ขึ้นต้นด้วย hypertensi…. ที่เป็น Primitive concept หลายๆ คำดูเป็น problem list หรือ provisional diagnosis
Disorder ที่ขึ้นต้นด้วย hypertensi… ที่เป็น defined concept ดูเป็น final diagnosis มากกว่า

แยกข้อมูลที่ต้องการใส่ตารางที่เล็กลง

เลือกข้อมูลจากตารางใหญ่ (description) ไปตาราง description_disorder เพื่อให้ขนาดตารางที่จะใช้ค้นหาเล็กลง จะค้นหาได้เร็วขึ้น

SQL script ตัวอย่างจะเลือกแต่ Defined concept แล้วเอา (disorder) ที่ต่อท้ายออก (ไม่อย่างนั้นถ้าค้นคำ ‘disorder’ ทุกอย่างจะออกมาจนระบบช้า)

INSERT INTO description_disorder (
id,
effectiveTime,
active,
moduleId,
conceptId,
languageCode,
typeId,
term,
caseSignificanceId
)
SELECT
d.id,
d.effectiveTime,
d.active,
d.moduleId,
d.conceptId,
d.languageCode,
d.typeId,
REPLACE( d.term, ' (disorder)', '' ) AS term,
d.caseSignificanceId AS caseSignificanceId
FROM
description AS d
LEFT JOIN concept AS c ON c.id = d.conceptId
WHERE
d.active = 1
AND c.definitionStatusId = '900000000000073002' -- Defined
AND d.conceptId IN (
SELECT
d.conceptId
FROM
description AS d
LEFT JOIN concept AS c ON c.id = d.conceptId
WHERE
d.active = 1
AND d.typeId = '900000000000003001' -- Fully specified name
AND TRIM ( d.term ) LIKE '%(disorder)'
AND c.definitionStatusId = '900000000000073002' -- Defined
)

Create full-text index

CREATE FULLTEXT CATALOG DescriptionFTDisorder;
CREATE FULLTEXT INDEX ON dbo.description_disorder(term)
KEY INDEX PK_3222c9020cb4992d477a040e445
ON DescriptionFTDisorder
WITH CHANGE_TRACKING AUTO;

ER Diagram

ตัวอย่าง ER Diagram บางส่วน ขอใส่ความสัมพันธ์เฉพาะส่วนที่กล่าวถึง จะเห็นข้อมูลว่าหน่วยเล็กที่สุดของ SNOMED-CT คือ Concept ซึ่ง xxx_id ทั้งหมดจะเป็น ConceptID (ยกเว้น row_id เป็น primary key)

ER Diagram บางส่วน

SQL Full-Text Search

หลังจากเตรียมข้อมูลก็พร้อม search แล้ว ด้วยข้อจำกัดของ SQL Server Full-Text Search ถ้าจะ search ให้ได้คำที่ตรงกับที่คิดไว้ แพทย์ควรค้นหาด้วยวิธีพิมพ์แค่ส่วนต้นของคำ คั่นด้วยวรรค แล้วต่อด้วยส่วนต้นของคำต่อไป เช่น อยากได้ Acute kidney injury (AKI) ให้พิมพ์ acute kidn inj

จากตัวอย่าง Acute kidney injury หรือ Acute renal failure จะเห็นอีกข้อดีหนึ่งของ SNOMED-CT คือมีโอกาสเจอ คำที่แพทย์ต้องการมากกว่า เพราะมีทั้ง FSN (Fully Specified Name) และ Synonym

SELECT
Rank as rank,
d.id as descriptionId,
d.conceptId as conceptId,
d.term as term,
d.pid as rowId,
d.typeId AS typeId
FROM description_disorder AS d
INNER JOIN CONTAINSTABLE
( description_disorder, term, '"acute* kidn* inj*"', 50)
AS KEY_TBL ON d.pid = KEY_TBL.[KEY]
-- เป็น prefix match (start with)
-- ระวัง SQL injection
ตัวอย่างผลจากความต้องการหา acute kidney injury
ตัวอย่างผลจากความต้องการหา acute renal failure

ก่อนส่งผลการค้นหาไปหน้าบ้าน ควรจะเรียงลำดับตามความยาวข้อความ เอาคำสั้นขึ้นก่อน มักจะตรงกับคำที่ผู้ใช้คิดอยู่ในใจมากกว่า

ตัวอย่าง full text search จาก SQL practical guide แนะนำให้เรียงผลการค้นหา ข้อความสั้นขึ้นก่อนเช่นกัน

ทดลองค้นหาด้วย​ SQL Full-Text Search จาก Web Frontend

สำหรับการทดสอบใช้
Database
: SQL server 2019
Backend: NestJS v.8
โดย Database และ Backend deploy อยู่ใน VMWareเดียวกัน มีขนาด 4vCPU, RAM 8GB, Disk SSD
โดย Front-end ทดสอบง่าย ๆ โดยรันบน localhost ต่อไปยัง backend โดยใช้ debounce time สำหรับแต่ละตัวอักษรที่ 50 ms

ตัวอย่าง: Neonatal sepsis มีข้อสังเกตคือ

ตัวอย่าง 1: ตั้งใจหา Neonatal Sepsis
  • ผลการค้นหาเจอ Transient neonatal neutropenia due to neonatal bacterial sepsis ขึ้นมาก่อน เพราะ full text search ได้ rank สูงกว่า ส่วนคำที่เราต้องการเป็นคำที่ 2: Neonatal sepsis จะเป็นคำที่สั้นที่สุดใน rank ต่อมา
  • HTTP Response time 688 ms ถือว่าสูง สำหรับใช้งานแค่คนเดียว
  • GET request สีแดงเป็นการยกเลิก HTTP request ของ frontend ก่อนจะได้ response (ไม่ใช่ error)
  • การใช้ MSSQL Full-text search ได้ผลที่น่าพอใจสำหรับการ Setup เบื้องต้น อาจสามารถปรับ performance เพิ่มเติมด้วยการจัดการ RAM และ TempDB ให้เหมาะสมสำหรับ Full Text Search

RediSearch

การจัดการ Tuning MS SQL Full Text Search ต้องอาศัยความชำนาญและภาระในการดูแลสูง จึงลองนำ RediSearch มาใช้แทน
กรณีนี้สามารถออกแบบให้ใช้ Redis เป็น primary database ได้ และจะทำ persistence หรือไม่ก็ได้ เนื่องจากไม่ได้เก็บ transactional data

ตัวอย่างการสร้าง RediSearch Full-text Index และค้นหา
สร้าง Full-text index ใน RediSearch (ขอแบ่งบรรทัดให้อ่านง่ายขึ้น)

FT.CREATE <index-name> ON HASH PREFIX 1 <key prefix pattern>
SCORE 0.25 NOHL STOPWORDS 0
SCHEMA
id TAG
effectiveTime NUMERIC
active TAG
moduleId TAG
conceptId TEXT NOSTEM WEIGHT 10
typeId TAG
term TEXT NOSTEM WEIGHT 50
caseSignificanceId TAG
# Reference: https://redis.io/commands/ft.create/

ก่อนเอาข้อความ search term ไปค้นหาใน Redis ควรเตรียมข้อมูลก่อน เพื่อให้ได้ผลลัพธ์ที่ต้องการ
ตัวอย่างจะอธิบายการเตรียมข้อมูลและการเอา keyword มาประกอบกัน

1. แปลง whitespace ที่มีหลายตัวติดกันให้เหลือตัวเดียว จากนั้นแยกคำออกเป็น array

const termList = userInputText.trim().replace(/\s\s+/g, ' ').split(' ')

2. แยก keyword ที่ยาว 1 ตัวอักษร ถือว่าไม่สำคัญ เอาไว้เป็น optional สำหรับเพิ่มคะแนนการค้นหา

const singleTerms = termList.filter(str => str.length === 1)

3. Escape special character เหล่านี้

[',', '.', '<', '>', '{', '}', '[', ']', '"', "'", ':', ';', '!', '@', '|'],
['#', '$', '%', '^', '&', '*', '(', ')', '-', '+', '=', '~', '/', '?', '`']

4. นำ array มารวมกัน โดยใช้ infix wildcard (*term*)และ optional (~term) ในส่วนนี้ปรับแต่งได้ตามต้องการ

const keyword = searchTerms.map(term => `*${term}* ~${term}*`).join(' ') +
(singleTerms.length ? ` ` + singleTerms.map(term => `~${term}`).join(' ') : '') +
` @effectiveTime:[-inf ${currentUnixTime}]`
  • *term* คือ Infix match
  • ~term* คือ Boost rank of prefix match, ถ้า match ทั้ง infix และ prefix, prefix match ควรจะได้คะแนนมากกว่า
  • ~t คือ Boost rank of single character ถ้าเจอแบบ exact match (optional)

5. ค้นหาด้วย RediSearch

FT.SEARCH <index-name> <keyword> WITHSCORES SCORER DISMAX LIMIT 0 50
# Reference https://redis.io/docs/stack/search/reference/query_syntax/

การ Scoring ของ RediSearch ถ้าไม่ระบุจะเป็น TF-IDF (default) แต่การค้นหา disorder เป็นคำสั้นๆ ไม่ใช้ลักษณะแบบเอกสารยาวๆ จึงไม่อยากให้ทำ inverse document frequency จึงตัด TD-IDF และ BM25 ออกไป
กรณีนี้เลือกใช้ DISMAX จะเป็น scoring function ที่ให้ค่าตรงไปตรงมามากกว่า อ้างอิง: https://redis.io/docs/stack/search/reference/scoring/

การค้นแบบ Full-text search query ประสิทธิภาพดีมากๆ ใช้เวลาน้อยกว่า 50 ms ใน network ภายในประเทศ

Benchmark ที่เห็นเป็นตัวเลขคร่าวๆ ไม่ได้ทำ performance test อย่างเป็นระบบ, ไม่ต้อง debounce input ก็ได้

ทำไมเลือกใช้ RediSearch มากกว่า Elastic Enterprise Search (Elastic App Search)ในเคสนี้

  • ข้อมูล SNOMED-CT ลักษณะเป็น static data (master data) ใช้พื้นที่เก็บคงที่ ไม่ได้โตขึ้นตามการใช้งาน จำกัดขนาด memory ได้
  • In memory database เร็วแรง 🚀
  • สามารถต่อยอดไปใช้ RedisGraph ซึ่งเป็น In-Memory Graph Database ที่เหมาะกับรูปแบบความสัมพันธ์ของ SNOMED Concept น่าจะได้ประสิทธิภาพที่ดีเช่นกัน

Comparison

ในตัวอย่างยังไม่ได้ทำ comparison หรือ performance benchmark อย่างเป็นระบบ แต่การทดสอบเบื้องต้นพบว่า RediSearch ให้ความเร็วมากที่สุด

เอกสารจาก Redis “Buyer’s Guide for
Real-Time Search Engines
” แสดงให้เห็นว่า Elasticsearch จะช้ากว่า RediSearch แบบชัดๆ เมื่อ operation/second หนักถึงระดับหนึ่ง ซึ่งในโรงพยาบาลน่าจะใช้กันไม่ถึง ทีม software developer อาจจะพิจารณาได้ทั้ง Elasticsearch และ RediSearch จากปัจจัยและความเหมาะสมในด้านอื่นๆ

Comparison จากเอกสารของ Redis

ประโยชน์ที่จะได้

เมื่อเราทำให้ผู้ใช้งานรู้สึกดีได้แล้ว ทั้งเรื่อง keyword ที่ดีของ SNOMED-CT และ application performance สิ่งที่ได้มาคือข้อมูลที่อยู่ในรูปแบบที่คอมพิวเตอร์เข้าใจ เก็บรูปแบบ SCTID (SNOMED CT Identifier) และ SCTID จะมีความสัมพันธ์กับข้อมูลอื่นๆ ขอแนะนำการใช้ประโยชน์ในเชิง software/ application ต่อไป

Keyword/ Synonym/ Abbreviation

Search term ที่มีแต่แรก (Out of the box) ตัวอย่าง: ค้นหาด้วยคำย่อมาตรฐาน COPD (แต่คำย่อของ HT,HTN (hypertension) ไม่มี)
ถ้าเทียบกับ ICD-10 แล้ว SNOMED-CT เหมาะกว่ามาก

ค้นหาด้วยคำย่อมาตรฐาน เช่น COPD, แต่ละผลลัพธ์ได้ SCTID ต่างกัน ถือว่าต่างโรคกัน
Suggestion ที่เหมาะสม ทำให้แพทย์คิดใช้ความคิดมากขึ้น มีโอกาสลง Early/Late-onset neonatal sepsis ที่จำเพาะ เหมาะสมกว่า neonatal sepsis

“Is a” suggestion

ความสัมพันธ์ของ concept ต่างๆ อยู่ในรูปแบบกราฟ, เมื่อเราดู Is a ในความสัมพันธ์ข้อง Bacterial pneumonia จะพาไปหาการวินิจฉัยที่แคบลง (พาไปหา source, more specific) ซึ่งยิ่งแคบหรือจำเพาะเจาะจง ยิ่งเป็นการเก็บข้อมูลที่เหมาะกับการสรุป Discharge summary

ภาพจากคู่มือ SNOMED-CT ภาษาไทย

ภาพตัวอย่างความสัมพันธ์ที่เริ่มจาก Bacterial pneumonia [53084003] โยงไปทาง Lung (body structure), Infectious process (pathology) ถ้ามองกลับกัน ถ้าเราสงสัยว่าปีนี้มี infection ในปอดเท่าไหร่และอยากได้ fungus, virus ด้วย จะใช้วิธีค้นหาตามความสัมพันธ์ได้

ตัวอย่าง Bacterial pneumonia
Suggestion ที่จะขึ้นให้ผู้ใช้เลือกการวินิจฉัยโรคที่จำเพาะมากขึ้น จะเป็นประโยชน์ทั้งกับทีมที่ดูแลคนไข้ การสรุปการรักษา การเบิกจ่าย เป็นต้น

กรอบสีแดงเป็น Is a suggestion, กรอบสีฟ้าเป็น body site (body site ขึ้นให้ดูว่าสามารถเอาไปทำ data analytic ต่อได้)

ตัวอย่าง Neonatal sepsis
ลงข้อมูลครั้งแรกวินิจฉัยได้จาก clinical, lab, imaging เบื้องต้น ผ่านไป 3 วัน เมื่อสามารถระบุชื่อเชื้อได้ แพทย์สามารถลงข้อมูลให้จำเพาะมากขึ้นได้ อีกทั้งชื่อเชื้อโรคก็ยังมีประโยชน์กับทางสถิติ และการติดตามผู้ป่วยหลังออกจากโรงพยาบาล

ตัวอย่างที่คล้ายกัน Is a relation แนะนำให้ specific microorganism ได้, เหมาะกับ discharge summary

ตัวอย่าง Fracture

  • ตัวอย่างกระดูกท่อนใหญ่หัก แบ่งได้หลายลักษณะ
  • SNOMED-CT suggestion ทำให้ แพทย์คิดมากขึ้น ระบุให้ละเอียดขึ้น [open/close; position: neck, head, shaft; type: stress, atypical, multiple]
  • การอนุญาตให้แพทย์ลงหลายวินิจฉัยได้ ยิ่งได้ข้อมูลเพิ่มมากขึ้น ดีกว่า ICD-10 มากๆ เมื่อต้องการ summary ค่อยมา map/ suggestion เป็น ICD-10

สรุป

  • ข้อดีของ SNOMED-CT สำหรับการลง Diagnosis มีเยอะมาก มีประโยชน์กับแพทย์ผู้ใช้งาน ทีมรักษาผู้ป่วย การบันทึกสถิติ
  • ICD-10 อย่างเดียวเป็นแค่กลุ่มโรค ไม่เหมาะสำหรับเป็นคำวินิจฉัยโรค เนื่องจากไม่มีความลำเอียดพอ รายละเอียดไม่พอ เช่นไม่บอกข้างซ้ายขวา ความรุนแรงของโรค แต่ SNOMED-CT ทำได้
  • RediSearch เหมาะสมกับการเป็น Full-text database มากที่สุด (ในบทความนี้) ทั้งเรื่องความเร็ว ค่าใช้จ่าย การใช้งานง่าย
  • การใช้ Redis ไม่สามารถค้นหาด้วยภาษา ECL (SNOMED CT Expression Constraint Language) ได้เมื่อเทียบกับเครื่องมืออื่นๆ ที่มีอยู่แล้ว ส่วนการเปรียบเทียบ performace กับ snowstorm ก็น่าลอง
  • การทำให้แพทย์อยากที่จะใช้ ต้องสร้าง UX (user experience) ให้ดีรอบด้าน ไม่ทำให้แพทย์รู้สึกเพิ่มภาระงาน ส่วน UX ด้าน performance เป็นเรื่องที่ทีม software developer ทำให้ดีได้

Programming Tips

Why RediSearch ? (technical)

  • RediSearch feature เพียงพอต่อการใช้งาน เช่น ทำ Prefix Tries & Suffix Tries ได้, Wildcard match, Fuzzy matching (พิมพ์ผิดบางตัวก็หาเจอ) => เมื่อ search term ยาวถึงระดับนึง ตั้ง Levenshtein distance เพิ่มเป็นขั้นบันได [RediSearch Query Syntax], ส่วนระบบ Full-text search scoring ใช้ DISMAX scoring ที่ตรงไปตรงมาน่าจะเหมาะกว่าอย่างอื่น
  • การเอาข้อมูลที่ transform แล้วเข้า Elastic app search ผ่านทาง REST API ที่มีข้อจำกัดต่อ batch จึงต้องเขียนโค้ดช่วยเอาข้อมูลเข้าหรือออก
  • ระบบ scoring ของ Elastic app search ปรับแต่งได้เยอะกว่ามาก แต่กรณีนี้ยังไม่จำเป็นต้องใช้ก็ได้ เช่น boost, fecets, precision tuning >> เคสนี้อาจจะได้ใช้กับ order laboratory, radiology, medication หรือหาข้อมูลวิจัย ที่ต้องการความแม่นยำสูงกว่า
  • Redis cloud ราคาถูกกว่า Elastic cloud, Redis cloud 100 MB = 7 USD/month จะได้ RediSearch, RedisGraph, RedisJSON

Frontend tips

User input debounce: เป็นการตั้งหน่วงเวลาช่วงระยะเวลาหนึ่งก่อนเริ่มเรียกข้อมูลจากฐานข้อมูล กรณีที่ผู้ใช้พิมพ์ข้อความต่อเนื่องแล้วเว้นการพิมพ์มากกว่าระยะเวลาหนึ่ง เช่น 0.3 วินาที จากนั้นจะเริ่มเรียกขอข้อมูลจาก backend & database การใช้ RediSearch สามารถตั้ง debounce สั้นๆ ได้ เช่น 0.05-0.1 วินาที

Abort controller: เป็นการยกเลิก HTTP request ก่อนหน้า เมื่อมี HTTP request ใหม่แล้วยังไม่ได้ response จากของเก่า แล้ว backend จะไปดักอีกที

Backend tips

เมื่อ Frontend ส่ง abort signal มาแล้วฝั่ง backend ควรจะยกเลิกการทำงานนั้น ที่ทำงานอยู่แต่ยังไม่เสร็จ ในกรณีนี้คือการ query database เพื่อประหยัด resource และไม่ให้งานที่ยังไม่เสร็จกองไว้จน resource หมด

เรื่อง cancel HTTP request, cancel SQL search ฝั่ง backend ค่อนข้างหายาก ไม่ค่อยมีคนเขียน ช่วงเรียน resident ได้มีโอกาสไปงาน Bangkok Javascript 1.0.0 ได้ฟังเรื่อง What happens when you cancel an HTTP request? — Younes Jaaidi สมัยนั้นยังไม่เคยเขียน NestJs หรือ Reactive programming มาก่อน, กลับมาดูอีกที ได้ลองใช้จริง น่าประทับใจ

อย่างแรกต้อง Think Reactive ตามที่อาจารย์ได้กล่าวเอาไว้, งานที่เราจะใช้ก็ดูเหมาะกับ RxJS

ตัวอย่างโค้ด Cancel HTTP request ของ NestJS

// เริ่มจากเขียน Interceptor ตั้งไว้ก่อน
@Injectable()
export class UnsubscribeQueryInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
if (context.getType() !== 'http') return next.handle();
const request = context.switchToHttp().getRequest<Request>();
const close$ = fromEvent(request.socket, 'close');
return next.handle().pipe(takeUntil(close$));
}
}
// เมื่อจะใช้ NestModule ก็กำหนดใน providers, หรือจะใส่ใน Root Module เขียนแบบในวีดีโอก็ได้
@Module({
controllers: [SnomedSearchController],
providers: [SnomedSearchService, { provide: APP_INTERCEPTOR, useClass: UnsubscribeQueryInterceptor }],
})
export class SnomedSearchModule {}
// ใน NestJS controller ใช้ QueryRunner แล้วดัก TimeoutError, QueryFailedError เพื่อ release connection
// Config SQL connection pool timeout ตามความเหมาะสม

อธิบายโค้ด: สร้าง Interceptor เอาไว้ เมื่อ frontend ยกเลิก HTTP request มา เราจะได้ HTTP close event, เมื่อเกิด observable close event มาเจอ takeUntil ก็จะ unsubscribe observable

--

--