नेवाडोस में सॉफ्टवेयर टीम के हिस्से के रूप में हम नेवाडोस ऑल टेरेन ट्रैकर® के लिए एक संचालन और निगरानी मंच का निर्माण कर रहे हैं। सोलर ट्रैकर एक उपकरण है जो सौर पैनल को सूर्य की ओर उन्मुख करता है। प्रत्येक सौर ट्रैकर लगातार हमारे प्लेटफ़ॉर्म पर स्थिति की जानकारी और रीडिंग भेजता है, जैसे कि वर्तमान कोण, तापमान, वोल्टेज इत्यादि और हमें विश्लेषण और विज़ुअलाइज़ेशन के लिए इस जानकारी को संग्रहीत करने की आवश्यकता है। यदि ट्रैकर को हर 5 सेकंड में डेटा भेजने के लिए कॉन्फ़िगर किया गया है, तो हमारे पास प्रति दिन प्रति ट्रैकर 17,280 डेटा पॉइंट, प्रति ट्रैकर प्रति माह 518,400 डेटा पॉइंट हैं। यह बहुत सारी जानकारी का सार है। इस प्रकार के डेटा को "टाइम-सीरीज़ डेटा" कहा जाता है और जहां तक सॉफ़्टवेयर में सभी जटिल समस्याओं का सवाल है, इसके लिए कई समाधान (टाइम सीरीज़ डेटाबेस) हैं। सबसे प्रसिद्ध इन्फ्लक्सडीबी और टाइमस्केलडीबी हैं। हमारे प्लेटफ़ॉर्म के लिए, हमने TDEngine के साथ काम करने का निर्णय लिया, जो एक अपेक्षाकृत नया उत्पाद है जो IoT अनुप्रयोगों के लिए अनुकूलित है और SQL क्वेरी भाषा के साथ काम करता है।
इस निर्णय के पक्ष में कई तर्क थे: TDEngine
इस लेख में, हम TDEngine डेटाबेस और तालिकाओं के सेटअप के बारे में जानेंगे और एक GraphQL स्कीमा कैसे तैयार करेंगे जो हमें विभिन्न ग्राहकों और अनुप्रयोगों से डेटा को क्वेरी करने की अनुमति देता है।
TDEngine के साथ शुरुआत करने का सबसे आसान तरीका उनकी क्लाउड सेवा का उपयोग करना है। TDEngine पर जाएं और एक खाता बनाएं। उनके पास कुछ सार्वजनिक डेटाबेस हैं जिनका हम उपयोग कर सकते हैं, जो डेमो डालने या प्रश्नों के साथ प्रयोग करने के लिए बहुत अच्छा है।
यदि आप TDEngine को स्थानीय रूप से चलाना चाहते हैं, तो आप विभिन्न स्रोतों से डेटा पुनर्प्राप्त करने और उन्हें डेटाबेस में भेजने के लिए डॉकर छवि और टेलीग्राफ का उपयोग कर सकते हैं, जैसे सिस्टम जानकारी, पिंग आँकड़े आदि।
version: '3.9' services: tdengine: restart: always image: tdengine/tdengine:latest hostname: tdengine container_name: tdengine ports: - 6030:6030 - 6041:6041 - 6043-6049:6043-6049 - 6043-6049:6043-6049/udp volumes: - data:/var/lib/taos telegraf: image: telegraf:latest links: - tdengine env_file: .env volumes: - ./telegraf.conf:/etc/telegraf/telegraf.conf
टेलीग्राफ कॉन्फ़िगरेशन के लिए आधिकारिक दस्तावेज़ और टेलीग्राफ पर TDEngine दस्तावेज़ देखें। संक्षेप में, MQTT विषय से जुड़ने के लिए यह कुछ इस तरह दिखेगा:
[agent] interval = "5s" round_interval = true omit_hostname = true [[processors.printer]] [[outputs.http]] url = "http://127.0.0.1:6041/influxdb/v1/write?db=telegraf" method = "POST" timeout = "5s" username = "root" password = "taosdata" data_format = "influx" [[inputs.mqtt_consumer]] topics = [ "devices/+/trackers", ]
सब कुछ स्थानीय रूप से सेट करने और डेटाबेस में जानकारी भरने की प्रतीक्षा करने के बजाय, हम इस लेख के लिए सार्वजनिक डेटाबेस का उपयोग करेंगे, जिसमें 5 प्रमुख अमेरिकी बंदरगाहों से जहाज की आवाजाही शामिल है।
डिफ़ॉल्ट रूप से, TDEngine में तालिकाओं में एक अंतर्निहित स्कीमा होती है, जिसका अर्थ है कि स्कीमा डेटाबेस में लिखे गए डेटा के अनुकूल होती है। यह बूटस्ट्रैपिंग के लिए बहुत अच्छा है, लेकिन अंततः, हम आने वाले डेटा के साथ समस्याओं से बचने के लिए एक स्पष्ट स्कीमा पर स्विच करना चाहते हैं। एक चीज जिसकी आदत पड़ने में थोड़ा समय लगता है वह है सुपर टेबल्स की उनकी अवधारणा (संक्षेप में "स्थिर")। TDEngine में टैग (कुंजियाँ) और कॉलम (डेटा) हैं। प्रत्येक कुंजी संयोजन के लिए, एक "तालिका" बनाई जाती है। सभी तालिकाओं को STable में समूहीकृत किया गया है।
vessel
डेटाबेस को देखते हुए, उनके पास ais_data
नामक एक STable है जिसमें बहुत सारी तालिकाएँ हैं। आमतौर पर, हम प्रति-तालिका के आधार पर क्वेरी नहीं करना चाहते हैं, लेकिन सभी तालिकाओं से संचित डेटा प्राप्त करने के लिए हमेशा STable का उपयोग करते हैं।
TDEngine में एक DESCRIBE
फ़ंक्शन है जो हमें किसी तालिका या STable की स्कीमा का निरीक्षण करने की अनुमति देता है। ais_data
में निम्नलिखित स्कीमा है:
STable में दो कुंजियाँ और छह डेटा कॉलम हैं। कुंजियाँ mmsi
और name
हैं। हम डेटा को क्वेरी करने के लिए नियमित SQL स्टेटमेंट का उपयोग कर सकते हैं:
SELECT ts, name, latitude, longitude FROM vessel.ais_data LIMIT 100; ts name latitude longitude 2023-08-11T22:07:02.419Z GERONIMO 37.921673 -122.40928 2023-08-11T22:21:48.985Z GERONIMO 37.921688 -122.40926 2023-08-11T22:25:08.784Z GERONIMO 37.92169 -122.40926 ...
ध्यान रखें कि समय-श्रृंखला डेटा आमतौर पर बहुत बड़ा होता है, इसलिए हमें हमेशा परिणामसेट को सीमित करना चाहिए। कुछ समय-श्रृंखला विशिष्ट फ़ंक्शन हैं जिनका हम उपयोग कर सकते हैं, जैसे PARTITION BY
जिसके परिणाम कुंजी द्वारा समूहित होते हैं और नवीनतम अद्यतन व्यक्तिगत कुंजी प्राप्त करने के लिए उपयोगी होते हैं। उदाहरण के लिए:
SELECT last_row(ts, name, latitude, longitude) FROM vessel.ais_data PARTITION BY name; ts name latitude longitude 2023-09-08T13:09:34.951Z SAN SABA 29.375961 -94.86894 2023-09-07T18:05:01.230Z SELENA 33.678585 -118.1954 2023-09-01T17:23:24.145Z SOME TUESDAY 33.676563 -118.230606 ...
मैं अधिक उदाहरणों के लिए उनके SQL दस्तावेज़ीकरण को पढ़ने की अनुशंसा करता हूँ। इससे पहले कि हम आगे बढ़ें, "प्रोग्रामिंग", "नोड.जेएस" पर जाएं और अपने TDENGINE_CLOUD_URL
और TDENGINE_CLOUD_TOKEN
वेरिएबल पुनः प्राप्त करें।
ग्राफक्यूएल इन दिनों काफी प्रसिद्ध है और इसके बारे में बहुत सारे अच्छे लेख हैं। हमने प्रौद्योगिकी को चुना क्योंकि हम विभिन्न स्रोतों से जानकारी एकत्र और संसाधित करते हैं और ग्राफक्यूएल हमें उन्हें पारदर्शी रूप से एक ही एपीआई में संयोजित करने की अनुमति देता है।
हम अद्भुत फास्टिफ़ाई फ्रेमवर्क (अब तक Node.js अनुप्रयोगों के लिए डिफ़ॉल्ट विकल्प) और मर्क्यूरियस एडाप्टर का उपयोग करेंगे। मर्क्यूरियस और फास्टीफाई की टीमों ने एक सहज अनुभव के लिए एक साथ काम किया और यह प्रदर्शन पर ध्यान देने के साथ ग्राफक्यूएल एपीआई का एक बेहतरीन विकल्प है। ग्राफक्यूएल नेक्सस स्कीमा और रिज़ॉल्वर बनाने/उत्पन्न करने का एक उपकरण है, इसलिए हमें सब कुछ हाथ से लिखने की ज़रूरत नहीं है।
कुछ सेटअप कोड आदि करना बाकी है, जिसे मैं यहां छोड़ दूँगा। आप GitHub - tdengine-graphql-example पर पूरा उदाहरण पा सकते हैं।
मैं इस लेख में दो चीजों के बारे में विस्तार से बताना चाहता हूं जो काफी विशिष्ट हैं:
TDEngine में एक Node.js लाइब्रेरी है जो हमें डेटाबेस से क्वेरी करने की अनुमति देती है। इससे कनेक्ट करना और प्रश्न भेजना आसान हो जाता है, दुर्भाग्य से प्रतिक्रियाओं के साथ काम करना थोड़ा मुश्किल होता है। तो हमने एक छोटा सा रैपर लिखा:
'use strict' import tdengine from '@tdengine/rest' import { tdEngineToken, tdEngineUrl } from '../config.js' import parseFields from 'graphql-parse-fields' const { options: tdOptions, connect: tdConnect } = tdengine tdOptions.query = { token: tdEngineToken } tdOptions.url = tdEngineUrl export default function TdEngine(log) { this.log = log const conn = tdConnect(tdOptions) this.cursor = conn.cursor() } TdEngine.prototype.fetchData = async function fetchData(sql) { this.log.debug('fetchData()') this.log.debug(sql) const result = await this.cursor.query(sql) const data = result.getData() const errorCode = result.getErrCode() const columns = result.getMeta() if (errorCode !== 0) { this.log.error(`fetchData() error: ${result.getErrStr()}`) throw new Error(result.getErrStr()) } return data.map((r) => { const res = {} r.forEach((c, idx) => { const columnName = columns[idx].columnName .replace(/`/g, '') .replace('last_row(', '') .replace(')', '') if (c !== null) { res[columnName] = c } }) return res }) }
यह एक TDEngine ऑब्जेक्ट लौटाता है जिसे GraphQL संदर्भ में पारित किया जा सकता है। हम मुख्य रूप से fetchData
फ़ंक्शन का उपयोग करेंगे जहां हम SQL क्वेरी पास कर सकते हैं और परिणाम को ऑब्जेक्ट की एक सरणी के रूप में वापस प्राप्त कर सकते हैं। TDEngine मेटाडेटा (कॉलम), त्रुटियां और डेटा अलग से लौटाता है। हम कॉलम को ऑब्जेक्ट की नियमित सूची में मैप करने के लिए मेटाडेटा का उपयोग करेंगे। यहां एक विशेष मामला last_row
फ़ंक्शन है। कॉलम last_row(ts)
, last_row(name)
आदि के रूप में लौटाए जाते हैं और हम last_row
भाग को हटाना चाहते हैं ताकि विशेषता GraphQL स्कीमा में 1:1 मैप हो। यह columnName.replace
भाग में किया जाता है।
दुर्भाग्य से TDEngine के लिए Postgraphile जैसा कोई स्कीमा जेनरेटर नहीं है और हम शुद्ध GraphQL स्कीमा लिखना और बनाए रखना नहीं चाहते हैं, इसलिए इसमें हमारी मदद के लिए हम Nexus.js का उपयोग करेंगे। हम दो बुनियादी प्रकारों से शुरुआत करेंगे: VesselMovement
और Timestamp
(जो एक अदिश प्रकार है)। दिनांक को टाइमस्टैम्प या दिनांक स्ट्रिंग के रूप में प्रदर्शित करने के लिए Timestamp
और TDDate
दो अलग-अलग प्रकार हैं। यह क्लाइंट एप्लिकेशन (और विकास के दौरान) के लिए उपयोगी है, क्योंकि यह तय कर सकता है कि किस प्रारूप का उपयोग करना है। asNexusMethod
हमें VesselMovement
स्कीमा में फ़ंक्शन के रूप में प्रकार का उपयोग करने की अनुमति देता है। हम मूल ts
टाइमस्टैम्प मान का उपयोग करने के लिए यहीं प्रकार परिभाषा में TDDate
हल कर सकते हैं।
import { scalarType, objectType } from 'nexus' export const Timestamp = scalarType({ name: 'Timestamp', asNexusMethod: 'ts', description: 'TDEngine Timestamp', serialize(value) { return new Date(value).getTime() } }) export const TDDate = scalarType({ name: 'TDDate', asNexusMethod: 'tdDate', description: 'TDEngine Timestamp as Date', serialize(value) { return new Date(value).toJSON() } }) export const VesselMovement = objectType({ name: 'VesselMovement', definition(t) { t.ts('ts') t.tdDate('date', { resolve: (root) => root.ts }) t.string('mmsi') t.string('name') t.float('latitude') t.float('longitude') t.float('speed') t.float('heading') t.int('nav_status') } })
समय-श्रृंखला प्रकारों के लिए, हम इंटरफ़ेस में संबंधपरक और समय-श्रृंखला प्रकारों के स्पष्ट पृथक्करण के लिए Movement
या Series
प्रत्यय का उपयोग करते हैं।
अब हम क्वेरी को परिभाषित कर सकते हैं। हम TDEngine से नवीनतम गतिविधियाँ प्राप्त करने के लिए एक सरल क्वेरी से शुरुआत करेंगे:
import { objectType } from 'nexus' export const GenericQueries = objectType({ name: 'Query', definition(t) { t.list.field('latestMovements', { type: 'VesselMovement', resolve: async (root, args, { tdEngine }, info) => { const fields = filterFields(info) return tdEngine.fetchData( `select last_row(${fields}) from vessel.ais_data partition by mmsi;` ) } }) } })
एपीआई का परीक्षण करने और स्कीमा का पता लगाने के लिए ग्राफीक्यूएल एक बेहतरीन टूल है, आप इसे मर्क्यूरियस में graphiql.enabled = true
पास करके सक्षम कर सकते हैं। क्वेरी के साथ, हम mmsi
द्वारा समूहीकृत जहाजों की नवीनतम गतिविधियों को देख सकते हैं। हालाँकि थोड़ा और आगे चलते हैं। GraphQL का सबसे बड़ा लाभ यह है कि यह क्लाइंट या एप्लिकेशन के लिए एक पारदर्शी परत है। हम कई स्रोतों से डेटा प्राप्त कर सकते हैं और उन्हें एक ही स्कीमा में जोड़ सकते हैं।
दुर्भाग्य से, मुझे व्यापक पोत जानकारी के साथ एक आसान/मुफ़्त एपीआई नहीं मिल सका। सिने है, लेकिन वे अपने वेसल प्रतिक्रिया में केवल name
, mmsi
और imo
प्रदान करते हैं (जो हमारे पास पहले से ही टीडीइंजिन में है)। उदाहरण के लिए, हम मानते हैं कि हमारे डेटाबेस में name
नहीं है और हमें इसे सिने से पुनर्प्राप्त करने की आवश्यकता है। imo
के साथ हम किसी जहाज के लिए CO2 उत्सर्जन के बारे में भी पूछ सकते हैं या किसी छवि, ध्वज या अन्य जानकारी को पुनः प्राप्त करने के लिए किसी अन्य एपीआई का उपयोग किया जा सकता है, जो सभी को Vessel
प्रकार में जोड़ा जा सकता है।
export const Vessel = objectType({ name: 'Vessel', definition(t) { t.string('mmsi') t.string('name') t.nullable.string('imo') t.list.field('movements', { type: 'VesselMovement' }) } })
जैसा कि आप यहां देख सकते हैं, हम TDEngine से समय-श्रृंखला डेटा के साथ एक सूची फ़ील्ड movements
शामिल कर सकते हैं। हम पोत की जानकारी प्राप्त करने के लिए एक और क्वेरी जोड़ेंगे और रिज़ॉल्वर हमें TDEngine और Synay से डेटा को संयोजित करने की अनुमति देता है:
t.field('vessel', { type: 'Vessel', args: { mmsi: 'String' }, resolve: async (root, args, { tdEngine }, info) => { const waiting = [ getVesselInformation(args.mmsi), tdEngine.fetchData( `select * from vessel.ais_data where mmsi = '${args.mmsi}' order by ts desc limit 10;` ) ] const results = await Promise.all(waiting) return { ...results[0][0], movements: results[1] } } })
🎉 और यहां हमारे पास हमारे द्वारा अनुरोधित एक विशिष्ट जहाज के लिए TDEngine से लौटने वाली एक कार्यशील ग्राफक्यूएल एपीआई है। getVesselInformation()
सिने से डेटा लाने के लिए एक सरल रैपर है। हम TDEngine परिणामों को movements
विशेषता में जोड़ देंगे और GraphQL बाकी का ध्यान रखेगा और स्कीमा में सब कुछ मैप करेगा।
किसी भी SQL डेटाबेस की तरह, हमें उपयोगकर्ता इनपुट से सावधान रहने की आवश्यकता है। उपरोक्त उदाहरण में हम सीधे mmsi
इनपुट का उपयोग करते हैं, जो इस क्वेरी को SQL इंजेक्शन के प्रति असुरक्षित बनाता है। उदाहरण के लिए, हम अभी इसे अनदेखा कर देंगे, लेकिन "वास्तविक दुनिया" अनुप्रयोगों में, हमें हमेशा उपयोगकर्ता इनपुट को स्वच्छ करना चाहिए। स्ट्रिंग्स को साफ करने के लिए आसपास कई छोटी लाइब्रेरी हैं, ज्यादातर मामलों में हम केवल संख्याओं (पेजिनेशन, लिमिट आदि) और एनम्स (सॉर्ट ऑर्डर) पर भरोसा करते हैं, जिन्हें ग्राफक्यूएल हमारे लिए जांचता है।
इस ओर ध्यान दिलाने के लिए दिमित्री ज़ेट्स को धन्यवाद!
कुछ चीजें हैं जो इस लेख के दायरे से बाहर हैं, लेकिन मैं उनका संक्षेप में उल्लेख करना चाहता हूं:
जब हमने प्रोजेक्ट शुरू किया, तो Nexus.js हमारी ग्राफक्यूएल स्कीमा तैयार करने के लिए सबसे अच्छा विकल्प था। हालांकि स्थिर और कुछ हद तक फीचर-पूर्ण , इसमें रखरखाव और अपडेट का अभाव है। पोथोस नामक एक प्लगइन-आधारित ग्राफक्यूएल स्कीमा बिल्डर है जो थोड़ा अधिक आधुनिक है और सक्रिय रूप से बनाए रखा गया है। यदि आप एक नया प्रोजेक्ट शुरू कर रहे हैं, तो मैं संभवतः Nexus.js के बजाय पोथोस का उपयोग करने की सलाह दूंगा।
इस ओर ध्यान दिलाने के लिए मो सैटलर को धन्यवाद!
जैसा कि आप ऊपर Vessel
रिज़ॉल्वर में देख सकते हैं, दोनों डेटा स्रोतों को तुरंत लाया और संसाधित किया जाता है। इसका मतलब यह है कि यदि क्वेरी केवल name
के लिए है, तब भी हम प्रतिक्रिया के लिए movements
लाते हैं। और यदि क्वेरी केवल movements
के लिए है, तो भी हम सिने से नाम प्राप्त करते हैं और संभावित रूप से अनुरोध के लिए भुगतान करते हैं।
यह एक ग्राफक्यूएल एंटी-पैटर्न है और हम केवल अनुरोधित डेटा लाने के लिए फ़ील्ड जानकारी का उपयोग करके प्रदर्शन में सुधार कर सकते हैं। रिज़ॉल्वर के पास चौथे तर्क के रूप में फ़ील्ड जानकारी होती है, लेकिन उनके साथ काम करना काफी कठिन होता है। इसके बजाय, हम अनुरोधित फ़ील्ड का एक सरल ऑब्जेक्ट प्राप्त करने और रिज़ॉल्वर तर्क को समायोजित करने के लिए graphql-parse-fields
उपयोग कर सकते हैं।
हमारे उदाहरण प्रश्नों में, हम डेटाबेस से सभी कॉलम लाने के लिए select *
उपयोग करते हैं, भले ही उनकी आवश्यकता न हो। यह स्पष्ट रूप से बहुत बुरा है और हम एसक्यूएल प्रश्नों को अनुकूलित करने के लिए उसी फ़ील्ड पार्सर का उपयोग कर सकते हैं:
export function filterFields(info, context) { const invalidFields = ['__typename', 'date'] const parsedFields = parseFields(info) const fields = context ? parsedFields[context] : parsedFields const filteredFields = Object.keys(fields).filter( (f) => !invalidFields.includes(f) ) return filteredFields.join(',') }
यह फ़ंक्शन ग्राफक्यूएल जानकारी से फ़ील्ड की अल्पविराम से अलग की गई सूची लौटाता है।
const fields = filterFields(info) return tdEngine.fetchData( `select last_row(${fields}) from vessel.ais_data partition by mmsi;` )
यदि हम ts
, latitude
और longitude
का अनुरोध करते हैं, तो क्वेरी इस तरह दिखेगी:
select last_row(ts, latitude, longitude) from vessel.ais_data partition by mmsi;
इस तालिका में केवल कुछ कॉलम होने से यह ज्यादा मायने नहीं रखता, लेकिन अधिक तालिकाओं और जटिल प्रश्नों के साथ, यह एप्लिकेशन के प्रदर्शन में बड़ा अंतर ला सकता है।
TDEngine में कुछ समय-श्रृंखला विशिष्ट एक्सटेंशन हैं जिनका उपयोग प्रदर्शन को बेहतर बनाने के लिए किया जाना चाहिए। उदाहरण के लिए, नवीनतम प्रविष्टि पुनः प्राप्त करने के लिए, एक पारंपरिक SQL क्वेरी:
SELECT ts, name, latitude, longitude FROM vessel.ais_data order by ts desc limit 1;
निष्पादित होने में 653ms लगते हैं, जबकि "TDEngine" क्वेरी को केवल 145ms लगते हैं:
SELECT last_row(ts, name, latitude, longitude) FROM vessel.ais_data;
प्रत्येक तालिका के लिए Last_row/first_row फ़ंक्शंस और अन्य कैश सेटिंग्स को अनुकूलित करने के लिए कॉन्फ़िगरेशन विकल्प हैं। मैं TDEngine दस्तावेज़ पढ़ने की अनुशंसा करता हूँ।
सरल संस्करण: इस आलेख में, हमने एक TDEngine समय-श्रृंखला डेटाबेस स्थापित किया है और क्लाइंट एप्लिकेशन को डेटा कनेक्ट करने और क्वेरी करने की अनुमति देने के लिए एक GraphQL स्कीमा परिभाषित किया है।
इसमें और भी बहुत कुछ है. हमारे पास एक पारदर्शी इंटरफ़ेस में रिलेशनल डेटा के साथ जटिल समय-श्रृंखला डेटा को संयोजित करने के लिए एक बॉयलरप्लेट प्रोजेक्ट है। नेवाडोस में, हम प्राथमिक डेटाबेस के रूप में PostgreSQL का उपयोग कर रहे हैं और ऊपर दिए गए movement
उदाहरण की तरह ही समय-श्रृंखला डेटा पुनर्प्राप्त करते हैं। यह एक ही एपीआई में कई स्रोतों से डेटा को संयोजित करने का एक शानदार तरीका है। एक अन्य लाभ यह है कि डेटा केवल अनुरोध किए जाने पर ही प्राप्त किया जाता है, जो क्लाइंट एप्लिकेशन में बहुत अधिक लचीलापन जोड़ता है। अंतिम लेकिन महत्वपूर्ण बात, ग्राफक्यूएल स्कीमा एक दस्तावेज़ीकरण और अनुबंध के रूप में काम करती है, इसलिए हम आसानी से "एपीआई दस्तावेज़ीकरण" बॉक्स पर टिक कर सकते हैं।
यदि आपका कोई प्रश्न या टिप्पणी है , तो कृपया ब्लूस्काई पर संपर्क करें या GitHub पर चर्चा में शामिल हों ।
यहाँ भी प्रकाशित किया गया है.