Масштабирование: создание высокопроизводительного PDF-экспортера данных о недвижимости

В динамичном мире управления активами в сфере недвижимости способность делиться, распечатывать и проводить комплексный анализ портфеля имеет решающее значение. Клиенты из сектора управления активами в сфере недвижимости поддержали потребность в инструменте, который мог бы обрабатывать большие данные, был бы простым в использовании и легким для совместного использования с другими сторонами. Им нужен был универсальный отчет, который включал бы все — местоположение, тип собственности, фотографии и аналитику активов, такую ​​как скидки, AVM и время продажи. Удовлетворение этой потребности было связано не только с удобством, но и с повышением эффективности и результативности их операций.

Войдите в Recognite, аналитическую фирму по недвижимости, управляемую искусственным интеллектом, которая предоставляет информацию об активах, специализируясь на проблемных кредитах, оптимизируя управление данными и улучшая процесс принятия решений на протяжении всего жизненного цикла недвижимости.

Проблема клиента и решение

Учитывая важность потребностей клиентов, команда Recognite решила разработать сопутствующее приложение, чтобы упростить и повысить эффективность экспорта анализа портфеля. Это приложение не только экспортирует эти отчеты, но и поддерживает тот же внешний вид, что и наше клиентское приложение, обеспечивая привычное и простое использование.

Приложение ориентировано на будущее по дизайну, что обеспечивает масштабируемость и потенциальное добавление новых функций. Например, в будущем может быть включена функция, позволяющая пользователям настраивать компоненты в соответствии со своими требованиями. Эта гибкость делает его мощным инструментом для клиентов, способным развиваться в соответствии с их меняющимися требованиями.

Применяя экспертные знания, мы решали проблемы, предоставляя решение, которое отвечает потребностям клиентов сегодня и готово адаптироваться к будущим потребностям.

Преодоление трудностей и выбор правильного стека технологий

Исследование рынка выявило множество потенциальных технологических стеков для решений для экспорта PDF, включая jsPDF, PDFMake и PDFKit. Хотя эти варианты жизнеспособны, они не совсем соответствуют конкретным потребностям проекта. Некоторые из них были ограничены внешними инструментами на основе браузера, другие ограничены их общими компонентами, а некоторые не могли обрабатывать безопасно сохраненные и подписанные изображения из-за своей базовой архитектуры.

Также были рассмотрены некоторые сторонние платные сервисы генератора PDF. Хотя у них были возможности для массового экспорта, им не хватало возможности использовать пользовательские компоненты, диаграммы и графики. Кроме того, стоимость была непомерно высокой, что со временем могло значительно повлиять на бюджет.

Это исследование не было напрасным, так как оно помогло определить проблемы, которые необходимо преодолеть:

  • Требование к решению, которое может масштабироваться для массового экспорта.
  • Необходимость интеграции решения с существующими компонентами, диаграммами и графиками для бесперебойного обслуживания клиентов.
  • Поиск доступного по цене решения, которое добавило бы ценности и легко интегрировалось бы в существующее семейство продуктов.
  • Спрос на инструмент, готовый к немедленному использованию, работающий в облаке и легко обновляемый в будущем.

В качестве бэкэнда были выбраны Node.js и Express, признанные за их легкость, эффективность и пригодность для создания масштабируемых сетевых приложений. Puppeteer, безголовый браузер, предоставляющий высокоуровневый API для управления Chrome или Chromium по протоколу DevTools, был выбран для задач, требующих автоматизации, таких как рендеринг PDF-файлов.

Серверная часть нашей платформы в значительной степени построена на базе облачных сервисов Google. Для этого приложения мы использовали PubSub для связи между службами, Protobufs для эффективной сериализации данных и облачное хранилище для хранения полезных данных и вывода PDF для каждого отчета.

Во внешнем интерфейсе был выбран Vue.js, прежде всего для повторного использования всех компонентов, графиков, таблиц, изображений и т. д. из клиентского приложения. Этот выбор позволил нам сохранить знакомый внешний вид нашей платформы, обеспечив единообразный пользовательский опыт во всех наших сервисах. Этот тщательный отбор технологий позволил вам преодолеть возникшие проблемы и предоставить решение, отвечающее потребностям клиентов.

Важность хорошей архитектуры программного обеспечения

Хорошая архитектура программного обеспечения является основой любого успешного программного проекта. Это обеспечивает гибкость, простоту обслуживания и перспективность. Имея это в виду, давайте углубимся в архитектуру приложения. Архитектура состоит из двух серверов, работающих одновременно внутри контейнера Docker в модуле Kubernetes, без какого-либо собственного пользовательского интерфейса. Когда клиент запрашивает экспорт одного или нескольких отчетов об активах на нашей платформе, запрос отправляется из нашего бэкэнда GoLang в приложение Node.js для каждого актива. Каждый из этих запросов содержит идентификатор задания и URI хранилища Google, где мы храним полезные данные отчета.

Первый сервер — это сервер Node.js, который отвечает за обработку основных операций. Второй сервер — это приложение Vue.js, которое отображает страницы на основе шаблонов и должно быть максимально простым. В настоящее время мы используем веб-пакет для объединения, но мы планируем перейти на Vite для повышения производительности в ближайшем будущем. Однако не стесняйтесь использовать любой компилятор — все они должны давать одинаковый результат.

Погружение в Кодекс

Первая часть процесса создания экспортера PDF включает в себя сторону приложения Node.js, вам нужно импортировать необходимые библиотеки и создавать для них новые экземпляры, когда это необходимо. Вам также необходимо сообщить приложению, где хранятся статические файлы, и определить номера портов.

./сервер.js

const fs = require("fs");
const express = require("express");
const puppeteer = require("puppeteer");
const { Storage } = require("@google-cloud/storage");
const { PubSub } = require("@google-cloud/pubsub");
const wrappers = require("google-protobuf/google/protobuf/wrappers_pb");
const pubSubClient = new PubSub();
const storage = new Storage();
const app = express();

const serverPort = 2020; // can be anything you like
const renderedPort = 2021; // should match compiler port
const renderHost = localHost; // match your vue app server
app.use("/dist", express.static("dist"));

Затем давайте запустим приложение Express и асинхронно прослушаем serverPort. В этот момент также запустите Puppeteer, назначьте его переменной браузера и передайте методу listenForMessages.

app.listen(serverPort, async () => {
  console.log(`Server started at: ${serverPort}`);
  try {
    let browser
    browser = await puppeteer.launch({
      args: ["--no-sandbox"],
      executablePath: '/usr/bin/google-chrome',// executablePath only needed when in the docker you need to remove when outside of docker.
    });
    await listenForMessages(browser);
  } catch (err) {
    console.error(err);
  }
});

В методе listenForMessages вы получаете сообщения из PubSub, используете данные сообщения для извлечения полезной нагрузки из хранилища, передаете полезную нагрузку в приложение Vue, используете Puppeteer для рендеринга страницы, загружаете сгенерированный отчет в формате PDF в хранилище и, наконец, создаете отчет. серверу о том, что работа выполнена, чтобы ею можно было поделиться с клиентом. Иметь один метод, делающий все это, — не лучшая идея, потому что это может затруднить сопровождение и отладку кода. Это связано с высокой сложностью и отсутствием модульности, что может затруднить понимание потока данных и зависимостей между различными частями кода. Поэтому лучше всего разделить его на столько методов, сколько необходимо, это улучшает читабельность и удобство сопровождения. Однако из-за области действия все эти отдельные функции по-прежнему необходимо вызывать из listenForMessages.

Первая часть, которую можно выделить, — это загрузка полезной нагрузки из Google Storage. Мы храним полезные нагрузки в двоичном формате, поэтому нам нужно было преобразовать их после получения полезной нагрузки, которая может не относиться к вам. Метод getJsonPayload загружает, десериализует, преобразует и сохраняет полезные данные в формате JSON, чтобы их можно было использовать в приложении Vue.

const getJsonPayload = async (bucketName, fileName, id) => {
  const binary = await storage
    .bucket(bucketName)
    .file(fileName)
    .download();
  const payload = {deserializer proto}.deserializeBinary(binary[0]).toObject();
  fs.writeFile(`./payloads/${id}.json`, JSON.stringify(payload), err => {
    if (err) {
      console.error(err);
      return;
    }
    console.log(`Local Payload File has been created`);
  });
  return payload;
};

Вторая часть, которую можно отделить от listenForMessages, — это отправка сгенерированных PDF-файлов в Google Storage или любое другое решение для хранения, которое вы в конечном итоге используете, но обязательно настройте их в соответствии с документацией по решению.

const sendReportToBucket = async (bucketName, fileName, id) => {
  await storage.bucket(`${bucketName}`).upload(`./pdfs/${id}.pdf`, {
    destination: fileName
  });
};

Последняя отделенная часть — это метод, в котором он сообщает серверной части о завершении задания.

const reportToPubSub = async (reportTopic, jobId, wrappedReportUri, error ) => {
  const reportMessage = new PDFReportEvent();
  reportMessage.setJobFileId(jobId);
  reportMessage.setReportUri(wrappedReportUri);
  const encodedMessage = reportMessage.serializeBinary();
  const dataBuffer = Buffer.from(encodedMessage);
  const topic = pubSubClient.topic(reportTopic);
  await topic.publish(dataBuffer);
};

И, наконец, давайте объединим все это в listenForMessages.

const listenForMessages = (browser) => {
  const pullAddress = `subscriptionNameOrId to get messages`;
  const reportAddress = `subscriptionNameOrId to send report`; 
  const pullSubscription = pubSubClient.subscription(pullAddress, {
// We limit the numbers of messages to 1 as PubSub messages get delivered in parallel, this can overwhelm the docker container
    flowControl: {
      maxMessages: 1
    }
  });
  const messageHandler = async message => {
    message.ack(); // aknowladge the message immidietly to so in case of error, the message does not get duplicated
    const pdfJobData={deserializer proto}.deserializeBinary(message.data).toObject(); // store all the needed data inside message.data and serielize it which than it getts deserialize in here using the same proto
    
    // Here we extract and define all the data that we need, they would be completely different for you 
    const payloadUri = pdfJobData.dataUri;
    const jobId = pdfJobData.jobFileId;
    const projectName = payloadUri.split("/")[0];
    const bucketName = `${projectName}-pdf-reports-${env}`;
    const reportUri = payloadUri.replace(".bin", ".pdf");
    const wrappedReportUri = new wrappers.StringValue();
    wrappedReportUri.setValue(reportUri);
    await getJsonPayload(`${projectName}-pdf-jobs-${env}`, payloadUri, jobId);
    const page = await browser.newPage();
    await page.setJavaScriptEnabled(true);
    await page.setDefaultNavigationTimeout(0);
    await page.goto(`http://localhost:${devPort}?id=${jobId}`, {
      waitUntil: "networkidle0",
    });
    await page.pdf({
      path: `pdfs/${jobId}.pdf`,
      margin: { top: "50px", right: "0", bottom: "20px", left: "25px" },
      printBackground: true,
      format: "A4",
      timeout: 0
    });
    await sendReportToBucket(bucketName, reportUri, jobId);
    await reportToPubSub(reportAddress, jobId, wrappedReportUri);
    await page.close();
    fs.unlink(`./payloads/${jobId}.json`, err => {
        if (err) throw err;
      });
    fs.unlink(`./pdfs/${jobId}.pdf`,err => {
        if (err) throw err;
      });
    };
  };
  pullSubscription.on("message", messageHandler);
};

Со стороны Node.js все готово, со стороны Vue.js не хватает одной части, чтобы собрать все вместе и заставить приложение работать — чтение полезной нагрузки. Есть много разных способов добиться этого. Мы пошли по этому пути, используя Axios (хотя вы можете использовать fetch), чтобы загрузить загруженный файл JSON в приложение Vue.

./src/App.vue

<template lang="pug">
v-app.ma-0.pa-0
    v-main(v-if="assetReport")
      YourTemplate(:assetReport="assetReport")
</template>
<script>
  import axios from 'axios';
  import YourTemplate from './templates/YourTemplate.vue';
  export default {
    name: 'app',
    components: {
      YourTemplate
    },
    data () {
      return {
        assetReport: null,
      }
    },
    created() {
      axios.get(`./payloads/${params.get('id')}.json`).then((json) => this.assetReport = json.data);
    },
  }
</script>

Итак, вот оно. Глубокое погружение во внутреннюю работу приложения, от высокоуровневой архитектуры до отдельных строк кода. Это был путь инноваций, адаптации и особого внимания к потребностям клиентов.

Краткое содержание

В заключение, путь к созданию высокопроизводительного PDF-экспортера данных о недвижимости подчеркивает инновации и целеустремленность Recognite. Мы решили задачи по эффективному управлению и совместному использованию крупномасштабных данных о недвижимости, создав инструмент, который отвечает потребностям клиентов для всестороннего анализа портфолио, пригодного для печати и совместного использования. Мы разработали приложение с Node.js, Express, Puppeteer, Google Cloud Services и Vue.js, которое не только способно выполнять насущные требования по созданию и экспорту отчетов с большими данными, но и достаточно гибко, чтобы адаптироваться к будущим потребностям. . Это решение, дополняющее нашу платформу аналитики недвижимости на основе искусственного интеллекта, предназначено для экспорта подробных отчетов о портфолио, сохраняющих знакомый внешний вид нашей основной платформы. Это обеспечивает единообразие взаимодействия с пользователем, облегчая клиентам обмен жизненно важными данными о недвижимости. Акцент на масштабируемость, эффективность и удобство использования привел к созданию приложения, которое легко согласуется с динамическими потребностями клиентов в управлении активами в сфере недвижимости. Создание этого экспортера PDF служит свидетельством нашей приверженности повышению операционной эффективности для наших клиентов, сохраняя их потребности в авангарде наших технологических достижений.

Био

Рами Фараджпур имеет более чем 5-летний опыт работы инженером-программистом. Его обширный опыт в области дизайна позволил ему беспрепятственно преобразовывать сложные проекты в код. Рами проявляет интерес к новым технологиям, таким как 3D, AR и AI, и часто интегрирует эти элементы в свои методы веб-разработки для улучшения пользовательского опыта и функциональности. С момента прихода в Recognite в 2021 году он применяет свой разнообразный набор навыков в качестве инженера-разработчика программного обеспечения.