Содержание


Использование IBM Cloud и PHP для создания базы данных резюме с функцией поиска, часть 2

Совершенствование приложения для поиска резюме с использованием масштабируемого хранилища и поиска по ключевым словам

Выполняйте запросы к проиндексированным данным для поиска резюме по конкретным навыкам

Comments

Серия контента:

Этот контент является частью # из серии # статей: Использование IBM Cloud и PHP для создания базы данных резюме с функцией поиска, часть 2

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Использование IBM Cloud и PHP для создания базы данных резюме с функцией поиска, часть 2

Следите за выходом новых статей этой серии.

В первой части я описал процесс создания оптимизированного для мобильных устройств, управляемого данными приложения с использованием микросреды PHP Slim в сочетании с Bootstrap и IBM Cloud®. Я также познакомил вас со службой индексации данных Searchly и продемонстрировал ее применение к задаче индексации информации, содержащейся в резюме соискателей. В этой части мы продолжим разработку приложения, реализовав возможность использовать службу Object Storage IBM Cloud для отправки загруженных резюме в масштабируемое, защищенное хранилище.

Я также покажу, как выполнять запросы к проиндексированным данным для поиска резюме по конкретным навыкам, и вы сможете приступить к использованию этого приложения для эффективного подбора кандидатов на открытые вакансии.

Запустить приложениеПолучить код с GitHub

Шаг 1. Инициализация службы Object Storage

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

Поскольку в IBM Cloud используется недолговечная файловая система, нельзя хранить загруженные резюме как «обычные» файлы (они просто исчезают при каждом развертывании новой версии приложения). Лучше использовать службу IBM Cloud Object Storage, предлагающую надежный и защищенный хостинг для хранения любых данных — включая резюме в формате PDF, загруженные через наше приложение.

Служба Object Storage поддерживает OpenStack Swift API и следует трехуровневой иерархии организации данных Swift: учетные записи, контейнеры и объекты. Вот как это работает:

  • Базовой единицей в иерархии является учетная запись. Учетные записи связаны с пользователями. Для получения доступа к учетной записи пользователь должен предоставить данные для аутентификации.
  • Учетная запись может содержать множество контейнеров, которые в широком смысле соответствуют каталогам в традиционной файловой системе.
  • Каждый контейнер может содержать множество объектов, которыми могут быть файлы или данные. Объекты могут иметь дополнительные, определяемые пользователем метаданные. Как правило, можно хранить неограниченное количество объектов.

Чтобы увидеть, как это работает, инициализируйте новый экземпляр службы Object Storage в IBM Cloud. Для этого войдите в IBM Cloud с использованием своей учетной записи и нажмите на панели кнопку Catalog. Из списка служб выберите Storage, затем Object Storage. Выберите бесплатный план и щелкните по кнопке Create для создания службы.

Рисунок 1. Создание службы
Создание службы
Создание службы

На странице с информацией о службе щелкните по раскрывающемуся меню Action в правом верхнем углу и выберите Создание контейнера.

Рисунок 2. Создание контейнера
Создание контейнера
Создание контейнера

Создайте новый контейнер с именем cvs.

Рисунок 3. Создание контейнера
Создание контейнера
Создание контейнера

На странице с информацией о службе перейдите на вкладку Service Credentials и получите учетные данные для службы по ссылке View Credentials, как показано на следующем рисунке.

Рисунок 4. Учетные данные для службы
Учетные данные для службы
Учетные данные для службы

Скопируйте значения ключей auth_url, region, userId и password в ключи object-store[url], object-store[region], object-store[user] и object-store[pass] в файле $APP_ROOT/src/settings.php.

Шаг 2. Поддержка загрузки файлов

Выполнив инициализацию службы хранения и сконфигурировав приложение для ее использования, обновите сценарий приложения для использования php-opencloud SDK, который включает клиент Object Storage и был загружен через Composer в части 1. Этот SDK предоставляет удобную PHP-оболочку вокруг методов Swift API, чтобы вы могли просто вызвать соответствующий метод — например, createContainer() или listContainers(), — а библиотека клиента обеспечит формулировку запроса и расшифровку ответа.

Для инициализации клиента Object Storage php-opencloud добавьте следующий код в файл $APP_ROOT/public/index.php перед функциями обратного вызова:

<?php
// инициализация приложения Slim - пропущено

// конфигурирование зависимостей
$container = $app->getContainer();

$container['objectstorage'] = function ($c) {
  $config = $c->get('settings');
  $openstack = new OpenStack\OpenStack(array(
    'authUrl' => $config['object-store']['url'],
    'region'  => $config['object-store']['region'],
    'user'    => array(
      'id'       => $config['object-store']['user'],
      'password' => $config['object-store']['pass']
  )));
  return $openstack->objectStoreV1();
};

Приведенный выше код использует контейнер внедрения зависимостей Slim для конфигурирования и подготовки к использованию клиента php-opencloud.

Затем обновите обработчик форм, включив дополнительный шаг по сохранению загруженного файла в Object Storage с использованием клиента:

<?php
// инициализация приложения Slim - пропущено

$app->post('/add', function ($request, $response, $args) {
  
  $post = $request->getParsedBody();
  $files = $request->getUploadedFiles();
  
  try {
  
    // проверка допустимости входных данных
    if (empty($post['name'])) {
      throw new Exception('No name provided');
    }

    if (empty($post['email']) || (filter_var($post['email'], 
      FILTER_VALIDATE_EMAIL) == false)) {
      throw new Exception('Invalid email address provided');
    }

    if (!empty($post['url']) && (filter_var($post['url'], 
      FILTER_VALIDATE_URL) == false)) {
      throw new Exception('Invalid URL provided');
    }
        
    // проверка загрузки файла
    if (empty($files['upload']->getClientFilename())) {
      throw new Exception('No file uploaded');
    }
    
    // проверка допустимости типа файла
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $type = $finfo->file($files['upload']->file);
    if ($type != 'application/pdf') {
      throw new Exception('Invalid file format, only PDF supported');    
    }
    
    // извлечение текста из PDF
    $pdf = $this->pdfparser->parseFile($files['upload']->file);
    $text = $pdf->getText();
    
    // добавление текста в индекс
    $document = array(
        'name' => strip_tags($post['name']),
        'email' => strip_tags($post['email']),
        'content' => $text,
        'url' => strip_tags($post['url']),
        'notes' => strip_tags($post['notes']),   
     );
     
    $params = array();
    $params['body']  = $document;
    $params['index'] = 'cvs';
    $params['type']  = 'doc';
    $indexerResponse = $this->indexer->index($params);
    $id = $indexerResponse['_id'];

    // сохранение PDF в хранилище объектов 
    $container = $this->objectstorage->getContainer('cvs');
    $stream = new Stream(fopen($files['upload']->file, 'r'));
    $options = array(
      'name'   => trim("$id.pdf"),
      'stream' => $stream,
    );
    $container->createObject($options);

    return $this->renderer->render($response, 'add.phtml', 
      array('router' => $this->router, 'id' => $id)
    );
    
  } catch (ClientException $e) {
    throw new Exception($e->getResponse());
  }
});

// другие обратные вызовы - пропущено

В первой части вы уже видели фрагменты этого кода — проверка загруженного файла, извлечение его содержимого и индексация ключевых слов с использованием Searchly. Дополнительный код начинается с использования метода getContainer() клиента Object Storage для получения ссылки на новый контейнер cvs, созданный на предыдущем шаге. Получив ссылку на контейнер cvs, нужно инициализировать новый поток из загруженного PDF-документа.

Этот поток передается методу контейнера createObject() в составе массива опций, который включает желаемое имя объекта в контейнере. Метод createObject() берет на себя задачу передачи и сохранения документа в Object Storage в виде поименованного объекта. Как вы увидите на шаге 5, имя объекта можно в любое время использовать как ключ для извлечения PDF-документа.

Шаг 3. Создание интерфейса поиска

Обеспечив сохранение и индексацию резюме, остается только создать интерфейс поиска, позволяющий быстро сканировать проиндексированный контент на наличие слов, отражающих требуемые навыки, и находить подходящие резюме кандидатов. Для этого добавьте маршрут /search и обработчик обратных вызовов в файл приложения Slim $APP_ROOT/public/index.php, как показано ниже:

<?php
// инициализация приложения Slim - пропущено

$app->get('/search', function ($request, $response, $args) {
  $params = $request->getQueryParams();
  $hits = array();
  $q = '';
  if (isset($params['q'])) {
    $q = trim(strip_tags($params['q']));
    if (!empty($q)) {
      $search = [
        'index' => 'cvs',
        'type' => 'doc',
        'body' => [
          'query' => [
            'multi_match' => [
              'query' => $q,
              'fields' => ['content', 'notes']
            ]
          ],
          'highlight' => [
            'fields' => [
              'content' => [
                'type' => 'plain',
                'fragment_size' => 40,
                'number_of_fragments' => 1
              ],
              'notes' => [
                'type' => 'plain',
                'fragment_size' => 40,
                'number_of_fragments' => 1
              ],                
            ]
          ]
        ]
      ];
      $results = $this->indexer->search($search);
      if ($results['hits']['total'] > 0) {
        $hits = $results['hits']['hits'];
      }
    }
  }  
  return $this->renderer->render($response, 'search.phtml', 
    array('router' => $this->router, 'hits' => $hits, 'q' => $q)
  );
})->setName('search');

// другие обратные вызовы - пропущено

Этот обратный вызов обрабатывает запросы для конечной точки /search и проверяет наличие строки запроса в URL-адресе. Если строка запроса найдена, он создает запрос Elasticsearch и использует метод клиента search() для генерирования и выполнения запроса к индексу Searchly. Метод search() возвращает PHP-массив, содержащий список найденных соответствий, и этот массив передается в сценарий отображения для вывода на экран. Searchly автоматически сортирует результаты для вывода сначала наиболее значимых.

Необходимо обратить внимание на два важных момента в отношении этого обработчика обратных вызовов:

  • Выполняемый поиск — это поиск multi_match, то есть Searchly будет запрашивать более одного поля для соответствий поисковому запросу. Запрос будет охватывать поле content, которое содержит контент, извлеченный из PDF-файла резюме, и поле notes, включающее дополнительные комментарии пользователя.
  • Поисковый запрос содержит дополнительные инструкции, чтобы результат включал не только идентификаторы подходящих документов, но и фрагменты (по 40 символов) с выделенными соответствиями — для предоставления пользователю улучшенного контекста.

Шаг 4. Вывод результатов поиска

Очевидно, нужен также сценарий отображения, чтобы выводить на экран форму поиска и все подходящие результаты. Для этой цели создайте сценарий $APP_DIR/templates/search.phtml со следующим содержимым (обратите внимание, в этом и последующих листингах удалены общие верхние и нижние области из предыдущего шага):

<div>   
  <form method="get" 
    action="<?php echo $data['router']->pathFor('search'); ?>">
    <div class="form-group">
      <input type="text" name="q" 
        value="<?php echo isset($data['q']) ? $data['q'] : ''; ?>" 
        class="form-control" placeholder="Search for...">
    </div>
    <div class="form-group">
      <button type="submit" name="submit" 
      class="btn btn-primary">Search</button>
    </div>          
  </form>
</div>

<div>
<?php if (isset($data['hits']) && count($data['hits'])): ?>
  <h4>Search Results</h4>
  <ul class="list-group row clearfix">
  <?php foreach ($data['hits'] as $doc): ?>
    <li class="list-group-item clearfix">
      <strong><?php echo $doc['_source']['name']; ?>
        </strong>
      <br />
      <?php echo (isset($doc['highlight']['content'][0])) ? '... ' . 
        $doc['highlight']['content'][0] . ' ... <i>(in content)</i>
        <br />' : ''; ?> 
      <?php echo (isset($doc['highlight']['notes'][0])) ? ' ... ' . 
        $doc['highlight']['notes'][0] . ' ... <i>(in notes)</i>
        <br />' : ''; ?>
      <br />
      <p class="text-center">
        <a href="<?php echo $data['router']->pathFor('download', 
          array('id' => $doc['_id'])); ?>" role="button" 
          class="btn-sm btn-success">Download CV</a> 
        <a href="mailto:<?php echo $doc['_source']['email']; ?>" 
          role="button" class="btn-sm btn-success">Email</a> 
        <?php if (!empty($doc['_source']['url'])): ?>
        <a href="<?php echo $doc['_source']['url']; ?>" 
          role="button" class="btn-sm btn-success">Connect</a> 
        <?php endif; ?>
      </p>
    </li>
  <?php endforeach; ?>
  </ul>
<?php endif; ?>
</div>

В этом сценарии отображения есть два основных элемента:

  • Форма поиска, которая предлагает пользователю текстовое поле для ввода одного или нескольких ключевых слов. Введенные пользователем данные отправляются в конечную точку /search как запрос GET.
  • Панель результатов поиска, которая показывает результаты выполнения обработчика /search вместе с именами кандидатов. Обратите внимание, помимо имени результат включает фрагмент с выделением найденного соответствия, а также указание на источник — резюме или дополнительные замечания.

На странице результатов также есть три кнопки. Первая ссылается на конечную точку /download, предоставляя пользователю возможность скачать соответствующее резюме в формате PDF из Object Storage. Вторая кнопка со ссылкой mailto: позволяет пользователю отправить электронное письмо на указанный адрес. Третья кнопка перенаправляет пользователя на онлайновый профессиональный профиль кандидата, если ссылка на него была указана при загрузке резюме.

Вот пример того, как это выглядит.

Рисунок 5. Результаты поиска с выделением соответствий
Результаты поиска с выделением соответствий
Результаты поиска с выделением соответствий

Шаг 5. Поддержка скачивания файлов

Одна из кнопок на странице результатов поиска указывает на конечную точку /download и предоставляет пользователю возможность скачать соответствующее резюме. Обратите внимание, URL-адрес /download включает как параметр идентификатор документа в индексе Searchly.

Вот код для обработчика /download, который использует передаваемый в URL идентификатор для подключения к службе Object Storage и извлечения соответствующего PDF-файла:

<?php
// инициализация приложения Slim - пропущено

$app->get('/download/{id}', function ($request, $response, $args) {
  $service = $this->objectstorage;
  $id = trim(strip_tags($args['id'])); 
  $stream = $service->getContainer('cvs')
                  ->getObject("$id.pdf")
                  ->download();
  header("Content-Disposition: attachment; filename=$id.pdf");
  header('Content-Type: application/pdf');
  header('Cache-Control: must-revalidate');
  header('Pragma: public');
  header('Content-Length: ' . $stream->getSize());
  ob_clean();
  flush();
  echo $stream;
})->setName('download');

// другие обратные вызовы - пропущено

Обработчик /download инициализирует новый клиент Object Service и получает ссылку на контейнер cvs в Object Storage через метод getContainer(). Получив контейнер, он переходит на уровень ниже для извлечения соответствующего объекта (PDF-файла) с использованием идентификатора документа, предоставленного в URL, через метод getObject(). После обнаружения объекта последним шагом является вызов метода download(), который извлекает бинарный контент файла как поток.

Чтобы сообщить клиенту пользователя о необходимости загрузить бинарный контент как файл, сценарий отправляет различные HTTP-заголовки, такие как Content-Disposition и Content-Types, за которыми следует контент потока. В конечном итоге запрос на загрузку файла будет активирован в клиенте пользователя, PDF-файл будет загружен из Object Storage и сохранен в локальном файловом хранилище пользователя.

Шаг 6. Возможность перезагрузки системы

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

Перезагрузка системы проста и включает только два шага:

  1. Удаление содержимого контейнера cvs в Object Storage
  2. Удаление содержимого индекса Searchly

Вот пример обработчика, выполняющего эти задачи. Его можно найти в исходном коде приложения в файле $APP_ROOT/public/index.php.

<?php
// инициализация приложения Slim - пропущено

$app->get('/reset', function ($request, $response, $args) {
  $params = [
    'index' => 'cvs'
  ];
  $this->indexer->indices()->delete($params);
  $this->indexer->indices()->create($params);
  $container = $this->objectstorage->getContainer('cvs');
  foreach ($container->listObjects() as $object) {
    $object->containerName = 'cvs';
    $object->delete();
  }
  $container->delete();
  $this->objectstorage->createContainer(array(
    'name' => 'cvs'
  )); 
  return $response->withStatus(301)->withHeader('Location', 'index');
})->setName('reset');

// другие обратные вызовы - пропущено

Этот обработчик обратных вызовов сначала инициализирует клиент Searchly и использует методы клиента delete() и create() для удаления индекса cvs вместе с его содержимым и создания его заново. Он также инициализирует клиент Object Storage, получает ссылку на контейнер cvs и проходит по его содержимому, удаляя каждый найденный файл. Пустой контейнер удаляется с использованием метода клиента delete(), затем создается новый пустой контейнер с использованием метода createContainer(). Конечным результатом является чистая доска.

Шаг 7. Развертывание на платформе IBM Cloud

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

---
applications:
- name: cvdb-[initials]
memory: 256M
instances: 1
host: cvdb-[initials]
buildpack: https://github.com/cloudfoundry/php-buildpack.git
stack: cflinuxfs2

Кроме того, нужно сконфигурировать пакет сборки для использования общедоступного каталога приложения как каталога web-сервера. Создайте файл $APP_ROOT/ bp-config/options.json со следующим содержимым:

{
    "WEB_SERVER": "httpd",
    "PHP_EXTENSIONS": ["bz2", "zlib", "curl"],
    "COMPOSER_VENDOR_DIR": "vendor",
    "WEBDIR": "public",
    "PHP_VERSION": "{PHP_56_LATEST}"
}

Скорее всего, вы также захотите автоматически получать из IBM Cloud учетные данные для служб Object Storage и Searchly. Это позволяет изменять пароли к службам или отключать/переподключать новые экземпляры служб без необходимости обновлять код приложения. Для этого измените код, добавив использование переменной IBM Cloud VCAP_SERVICES, как показано ниже:

<?php
// если среда IBM Cloud VCAP_SERVICES доступна
// изменить на учетные данные из IBM Cloud
if ($services = getenv("VCAP_SERVICES")) {
  $services_json = json_decode($services, true);
  
  $settings['settings']['object-store']['url'] = 
    $services_json["Object-Storage"][0]["credentials"]["auth_url"] . '/v3';
  $settings['settings']['object-store']['region'] = 
    $services_json["Object-Storage"][0]["credentials"]["region"];
  $settings['settings']['object-store']['user'] = 
    $services_json["Object-Storage"][0]["credentials"]["userId"];
  $settings['settings']['object-store']['pass'] = 
    $services_json["Object-Storage"][0]["credentials"]["password"];
  $settings['settings']['indexer']['url'] = 
    $services_json["searchly"][0]["credentials"]["uri"];
}

Теперь вы можете отправить приложение в IBM Cloud, а затем привязать службы Object Storage и Searchly, инициализированные для него ранее. Не забудьте использовать правильный идентификатор для каждого экземпляра службы, чтобы они корректно привязывались к приложению. Идентификатор службы можно получить на странице экземпляра службы на панели IBM Cloud.

Рисунок 6. Идентификаторы служб
Идентификаторы служб
Идентификаторы служб
shell> cf api https://api.ng.IBM Cloud.net
shell> cf login
shell> cf push
shell> cf bind-service cvdb-[initials] "Searchly-[id]"
shell> cf bind-service cvdb-[initials] "Object Storage-[id]"
shell> cf restage cvdb-[initials]

Теперь вы можете перейти к приложению по ссылке http://cvdb-[initials].myIBM Cloud.net и увидеть страницу приветствия. Если это не так, воспользуйтесь ссылками в начале этого раздела, чтобы узнать, как получить журнал отладки.

Заключение

В этом руководстве описано создание полезного, управляемого данными web-приложения для решения проблемы, с которой постоянно сталкиваются менеджеры по подбору и найму персонала — как быстро просеять объемный и постоянно растущий банк резюме кандидатов для эффективного получения краткого списка подходящих соискателей. Эта цель достигается сочетанием микросреды PHP с открытым исходным кодом и различных библиотек с масштабируемыми службами IBM Cloud для индексации данных, хранения и хостинга. Конечным результатом является масштабируемое, защищенное приложение, соответствующее современным стандартам использования мобильных и настольных систем, и в то же время достаточно гибкое, чтобы учитывать новые потребности специалистов по подбору и найму персонала.

Если вы хотите поэкспериментировать со службами, описанными в этом руководстве, начните с демонстрации приложения. Не забывайте, что это общедоступная демонстрация, поэтому вам не следует загружать конфиденциальную информацию (можно использовать функцию перезагрузки системы для удаления всех отправленных данных). Затем загрузите код из репозитория GitHub и внимательно изучите, как все это работает. Вы также можете перейти по ссылкам на связанные темы, чтобы узнать больше о различных службах и инструментах, используемых в этом руководстве.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Облачные вычисления
ArticleID=1051855
ArticleTitle=Использование IBM Cloud и PHP для создания базы данных резюме с функцией поиска, часть 2: Совершенствование приложения для поиска резюме с использованием масштабируемого хранилища и поиска по ключевым словам
publish-date=11072017