Рубрики
Без рубрики

Создайте свой собственный поисковую систему Node.js для Github Wikis

В этой статье мы собираемся узнать, как использовать Node.js, чтобы построить поисковую систему для Wikis на основе Markdown, как вики GitHub. знак равно

Автор оригинала: Rudolf Olah.

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

Оглавление

  • Задний план
    • Что такое вики?
    • Github Wiki как база знаний
    • Что такое поисковая система?
  • Почему нам нужна поисковая система для GitHub Wikis?
  • Создание поисковой системы
    • Предпосылки
    • Загрузка Github Wiki
    • indexer.js: индексирование содержимого вики GitHub
    • Модули и библиотеки нам нужны
    • Установка URL Wiki с аргументами командной строки
    • Прогуливаясь по каждой обработке файла
    • Переключение каждого файла в поисковые данные
    • Помечая содержимое
    • Разметка разбакивания в дереве
    • Группировка контента в разделы
    • Преобразовать содержимое в данные поиска
    • Законченный индексатор и графики вызовов
  • search.js: поиск по базе данных
  • Куда пойти отсюда

Задний план

Что такое вики?

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

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

Первый вики был WikiwikiWeb (который запускает C2 Wiki ), и Википедия является одним из крупнейших вики существовавших. Есть много других видов вики, и их было обнаружено, что есть пример места для организации знаний.

Github Wiki как база знаний

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

Нажмите здесь Чтобы узнать, как создать вики в Github.

Вики GitHub основан на плоском списке файлов Markdown в репозитории Git. Это означает, что Wiki имеет встроенный контроль версий и имеет простой в использовании язык форматирования.

Что такое поисковая система?

Узел js поисковая система

Поисковая система предоставляется поисковой запрос, а затем ищет список документов, файлов или любых других данных для всего, что связано с этим запросом. Например, когда вы ищете «автомобили» в Google, вы увидите результаты поиска для автосалон и фотографий автомобилей.

Есть две части к поисковой системе:

  • Индексатор создает индекс контента для поиска
  • Поиск Поиск по содержанию на основе запроса
Узел js поисковая система

Почему нам нужна поисковая система для GitHub Wikis?

GitHub не на самом деле не обеспечивает поисковой системе для Wikis (если, конечно, вам нравится загрузить файлы markdown wiki и используя grep для их поиска!). Это ограничивает полезность Wiki в качестве инструмента организации знаний и ограничит его читателям. Если вы не можете быстро найти полезную информацию, то каков момент документирования этой информации в первую очередь?

Наша поисковая система для Wikis на основе Markdown, таких как Wiki Github, стремится сделать вики более доступным и полезным и превратить его в краткую ссылку.

Создание поисковой системы

Мы собираемся сделать сверху вниз подход к созданию поисковой системы. Мы уже знаем, какой поиск состоит из (индексатора и поиска), и мы знаем, что мы собираемся преобразовывать файлы разметки в базу данных, которая может быть выполнена.

Предпосылки

Чтобы начать строить поисковую систему, нам понадобится несколько вещей:

  • Node.js и NPM (управляющий узел Packager)
  • Узел пакеты:
    • Commactmark , анализатор для Markdown, используемый для распределения каждой разметки файла, чтобы мы могли индексировать правильный текст, будь то элемент списка или заголовок или абзац или ссылка.
    • stemmer Для поиска «стебля» слова (например, стебель «поиска» – «поиск»), используемый для поворота слов в ключевые слова для поисковой системы.
    • ходить , для прогулки по дереву каталога и перечень файлов, используемых для поиска всех файлов Markdown в каталоге Wiki.
  • Файлы разметки из вики Github.

Готовый код для этого проекта доступен здесь: https://gitlab.com/Rudolfo/Libranian/tree/master.

Загрузка Github Wiki

Вы можете скачать копию вашего Github Wiki, клонируя так, как это в командной строке:

    git clone --depth 1 ssh://git@github.com/${USER}/${REPO}.wiki.git wiki

В качестве примера вы можете скачать Учебное Perl6 Wiki, что я настроил:

    git clone --depth 1 ssh://git@github.com/rudolfolah/learning-perl6.wiki.git wiki

Это будет клонировать вики в Вики каталог, и вы увидите несколько файлов Markdown.

indexer.js: индексирование содержимого вики GitHub

Теперь, когда у нас есть файлы Markdown из Wiki, мы можем начать писать индексатора.

Узел js поисковая система

Основной алгоритм, который мы реализуем это:

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

Модули и библиотеки нам нужны

Начнем со всех модулей, которые нам нужно импортировать.

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

    const fs = require('fs');
    const path = require('path');
    const walk = require('walk');

Затем нам нужно иметь возможность разбирать файл Markown в разные детали, такие как пункт, заголовок, элемент списка и ссылка. Библиотека JavaScript Commentmark делает это для нас и позволяет пройти через каждый узел в дереве, который составляет файл Markdown.

    const commonmark = require('commonmark');

Следующее, что нам нужно, это stemmer который:

    const stemmer = require('stemmer');

Последний модуль, который нам нужен для импорта, это Крипто Мы хотим генерировать хеш для каждого элемента контента, который мы индексируем с страниц Wiki. Это позволяет нам однозначно идентифицировать каждое содержание, которое позволяет, позже, применить методы релевантности.

    const crypto = require('crypto');

Установка URL Wiki с аргументами командной строки

Как упоминалось в шагах алгоритма, мы собираемся создавать ссылку на Wiki. Мы не можем извлечь URL URL Wiki из контента Markdown, поэтому ему нужно будет поставляться через аргументы командной строки.

    const wikiUrlPrefix = process.argv[2];

Прогуливаясь по каждой обработке файла

Мы собираемся использовать ходить Модуль для создания Уокер , который будет Пройдите через каждую файл Markdown в каталоге Wiki, Wikidir Отказ Мы создадим пустой стол Hash для хранения индексированных данных, другими словами, это база данных источника поиска, который мы накладываем:

    const wikiDir = 'wiki/';
    const walker = walk.walk('wiki/');
    let index = Object.create(null);

Для каждого файла в Wikidir мы собираемся проверить, заканчивается ли он Расширение файла .md , что скажет нам, стоит ли ли это FALLEDORDED MALDDONDATTED (это распространенная конвенция, которая используется).

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

    walker.on('file', function(root, fileStats, next) {
      const fileName = fileStats.name;
      if (fileName.indexOf('.md') !== -1) {
        const pathName = path.join(wikiDir, fileName);
        const content = fs.readFileSync(pathName).toString();
        index[fileName] = processFile(fileName, content);
      }
      next();
    });

Мы называем Далее () Перейти к следующему файлу в каталоге Wiki для проверки.

Нам также необходимо обрабатывать любые ошибки, которые отображаются во время прогулки по файлам:

    walker.on('errors', function(root, nodeStatsArray, next) {
      next();
    });

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

    walker.on('end', function() {
      let result = [];
      for (var fileName in index) {
        for (var i = 0; i < index[fileName].length; i += 1) {
          result.push(index[fileName][i]);
        }
      }
      console.log(JSON.stringify(result));
    });

Переключение каждого файла в поисковые данные

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

    function processFile(fileName, content) {
      let result = [];

Каждое содержание должно быть связано с ближайшим заголовком. Возможно, что страница Wiki Markdown не будет иметь никакого заголовка и будет иметь только заголовок (его имя файла) – это позволяет нам ассоциировать весь текст на странице с заголовком, действующим в качестве первого заголовка.

Мы также хотели бы разрушить название страницы Wiki в Теги ключевых слов. Каждый кусок текста, который мы выписываем с этой страницы Wiki, будет Начните с этими же тегами. Например, если название страницы это “привет мир”, то теги – это [«Привет», «Мир»] Отказ

    const title = fileName.replace('.md', '');
    const tags = breakIntoTags(title);

Затем нам нужно разобрать контент файла, который является форматированной разметкой на этой точке в дереве узлов. Каждый узел сообщит нам, какой тип форматирования усовершенствования это, такая как ли это ссылка или абзац. Мы инкапсулируем это в функцию, чтобы мы могли выбрать другой парсер, отличный от Commicark, если мы обнаружим, что реализация слишком медленная.

    const tree = contentToMarkdownTree(content);

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

    const processedContent = processContent(title, tree);
    for (var heading in processedContent) {
      const headingTags = breakIntoTags(heading);

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

    for (var i = 0; i < processedContent[heading].length; i += 1) {
      const item = processedContent[heading][i];
      const data = convertToSearchData(title, heading, tags, item);

После конвертации к поиску данных мы вставляем данные в нашу растущую базу данных поиска, а затем вернуть результат:

          result.push(data);
        }
      }
      return result;
    }

Помечая содержимое

Когда мы сломаем заголовок или содержимое раздела в теги ключевых слов, нам нужно очистить их, удаляя любые неистовые символы. Нам также нужно строить символы, поэтому теги соответствуют. Стеммер сократит слова к их стену, например «поиск» становится «поиск».

    function breakIntoTags(text) {
      let clean = text.replace(/[^a-zA-Z]/g, ' ');
      clean = clean.toLowerCase();
      clean = clean.split(' ');
      let tagsHash = Object.create(null);
      for (var i = 0; i < clean.length; i += 1) {
        if (shouldIgnoreWord(clean[i])) {
          continue;
        }
        const stemmed = stemmer(clean[i]);
        tagsHash[stemmed] = true;
        tagsHash[clean[i]] = true;
      }
      let tags = [];
      for (var key in tagsHash) {
        if (key.length > 0) {
          tags.push(key);
        }
      }
      return tags;
    }

Мы также используем HONGIGNOREWORD Функция, чтобы увидеть, если мы должны избегать обработки слова как действительный тег, такие вещи, как «« и «на» и «AN» И одно символы, такие как «A» или «E», не являются действительными тегами, потому что они часто встречаются и не имеют отношения к содержанию.

    function shouldIgnoreWord(text) {
      const stopWords = ['the', 'on', 'for', 'up', 'an', "'", 'to'];
      return text.length === 1 || stopWords.indexOf(text) !== -1;
    }

Разметка разбакивания в дереве

Чтобы преобразовать содержимое файла в дерево узлов Markdown Мы просто создаем новый анализатор, используя Commicalmark а затем вернуть результаты вызова функции анализа:

    function contentToMarkdownTree(content) {
      const reader = new commonmark.Parser();
      return reader.parse(content);
    }

Группировка контента в разделы

Теперь давайте погрузимся в то, как мы обрабатываем контент на основе дерева Markdown. Нам сначала нужно создать древесный ходунок и следить за текущим заголовком и текстом. Нам также нужно отслеживать разделы, которые мы уже собрали.

    function processContent(title, tree) {
      const walker = tree.walker();
      let event, node, child;
      let currentHeading = null;
      let currentText = null;
      let sections = {null: []};

Мы проходим по каждому узлу на дереве Markdown и проверьте, является ли это заголовком или, если он представляет собой буквальное значение (например, абзац, A ссылка или элемент списка). Когда узел является заголовком, мы используем функцию помощника, getnodechildrentext , чтобы убрать текст заголовка и сохранить его как текущий заголовок. Если это буквальный, мы группируем его в текущий раздел (после некоторой очистки, замена новых линий пробелами и сделав абзац нижнего кожуха):

    while ((event = walker.next())) {
      node = event.node;
      if (node.type === 'heading') {
        currentHeading = getNodeChildrenText(node);
      } else if (node.literal) {
        const text = node.literal.replace('\n', ' ').toLowerCase();
        if (sections[currentHeading]) {
          sections[currentHeading].push(text);
        } else {
          sections[currentHeading] = [text];
        }
      }
    }

Наконец, после того, как все разделы были сгруппированы, мы назначаем название страницы Wiki в качестве заголовки для любого контента, который не имел заголовок и возврата групп разделов:

      sections[title] = sections[null];
      delete sections[null];
      return sections;
    }

И вот функция помощника, getnodechildrentext Для создания Направляя правильно:

    function getNodeChildrenText(node) {
      let text = '';
      child = node.firstChild;
      do {
        text += child.literal;
      } while ((child = child.next));
      return text;
    }

Преобразовать содержимое в данные поиска

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

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

Данные, которые нам нужны:

  • Уникальный идентификатор для идентификации контента
  • Название страницы Этот контент принадлежит (пример: «Wiki Page»)
  • URL на страницу Wiki (пример: «Wiki-Page.html»)
  • Заголовок раздела, который принадлежит содержание (пример: «Некоторые раздел»)
  • URL-адрес на странице Wiki и глубоко связан с разделом (пример: «Wiki-Page.html # Seceed-раздел»)
  • Само содержимое
  • Теги ключевых слов, которые связаны с контентом и заголовком
    function convertToSearchData(title, heading, tags, item) {
      const subheadingUrl = heading.replace(/\s+/g, '-').replace(/[\/()]/g, '').toLowerCase();
      const id = generateId(title, heading, item.content);
    
      const titleUrl = `${wikiUrlPrefix}/${title.replace(' ', '-')}`;
      let headingUrlSuffix = heading.toLowerCase().replace(/[\/\(\),.]/g, '').replace(/ /g, '-');
      return {
        id: id,
        title: title,
        title_url: titleUrl,
        heading: heading,
        heading_url: title == heading ? titleUrl : `${titleUrl}#${headingUrlSuffix}`,
        content: item,
        tags: tags.concat(breakIntoTags(item)).concat(headingTags)
      };
    }

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

Это функция помощника, создавающая уникальный идентификатор, он зависит от Crypto.createhash Функция и использует алгоритм SHA-256 для всех аргументов, которые были предоставлены ему:

    function generateId() {
      const hash = crypto.createHash('sha256');
      hash.update.apply(hash, arguments);
      return hash.digest('hex');
    }

Законченный индексатор и графики вызовов

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

Исходя из всего кода выше, вот a Звоните график Это показывает зависимости между функциями:

Узел js поисковая система

Аннотации, которые я включал в карту диаграммы на алгоритм сверху вниз, который мы реализовали, и сопоставляем разделы, которые мы только что охватывали (сравните названия каждого раздела аннотации).

Вот еще одна версия графика вызова, который включает в себя импортные модули, такие как ФС , Крипто и Commactmark :

Щелчок здесь Чтобы просмотреть полный график вызова

Звоните график

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

search.js: поиск по базе данных

Теперь, когда мы создали индекс базы данных всех наших страниц Wiki ‘ Содержание, мы можем начать поиск через него. Это очень легко сделать Так как мы уже пометили содержание с ключевыми словами. Держать вещи Простое, мы сделаем поиск по ключевым словам по одному ключевое слово для соответствующего контента. Это Базовый поиск также отлично подходит для отладки индекса поиска, потому что вы можно увидеть, какие результаты отображаются или нет, а затем вы можете настроить indexer.js Чтобы добавить лучшие теги ключевых слов.

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

    const query = process.argv[2].replace('-', '').toLowerCase();

Затем мы загружаем базу данных, сгенерированную Indexer.js, я сохранил вывод из него в файл, называемый «| ». Что бы. Джон :

    const data = require('./whatever.json');

Мы используем тот факт, что Node.js может загрузить файлы JSON без явного создания анализатора JSON.

Далее мы начинаем создавать результаты поиска, затерев все данные в базе данных поиска:

    let result = {};
    for (var i = 0; i < data.length; i += 1) {
      const item = data[i];

Если мы найдем ключевое слово поиска в списке тегов для содержимого, мы включаем его в результаты поиска:

    if (item.tags.indexOf(query) !== -1) {
      result[item.id] = item;
      console.log('found in tags: ' + item.tags);

Если мы найдем ключевое слово поиска в фактическом контенте, мы также включаем его в результаты поиска:

      } else if (item.content.indexOf(query) !== -1) {
        result[item.id] = item;
        console.log('found in content: ' + item.content);
      }
    }

После этого мы просто повторяем все результаты поиска и распечатайте заголовок страницы, ближайшего заголовка и URL на страницу Wiki, на которой находится результат поиска, наряду с содержимым, который был найден в базе данных:

    console.log('#########');
    for (var id in result) {
      const item = result[id];
      console.log(`${item.title} - ${item.heading}`);
      console.log(`Link: ${item.heading_url}`);
      console.log();
      console.log(item.content);
      console.log('@@@@@@@@@');
    }

Куда пойти отсюда

Теперь, когда у вас есть поисковая система, вы создаете больше интерфейсов для доступа к базе данных. Один прохладный пример – это точка провисания/хипчата, где вы можете задать вопрос о боте, то он повернет вопрос в запрос, ищите ответ на вики Githi и вернуть ссылку на него.

Это то, на что я работал с коллеги, Suhail Dawood Во внутренней компании Hackhathon (который ustrene.com хранится ежемесячно). Мы позволили поиску в Wiki компании и Slack Chatbot использовали классификатор, чтобы сделать интерфейс поиска болееmerter. Чатбот увидел немедленное использование и был полезен в ответах, таких как «Какой пароль офиса WiFi?» и “Где логи сервера?”

Другим примером является строительство вики-ботов. Пользователи Wikipedia используют Wiki-Bots, чтобы автоматизировать очистку определенных страниц или найти страницы, которые имеют разбитые ссылки. Поскольку страницы GitHub Wiki не имеют оглавления, вы можете создать Wiki-Bot, который сканирует через страницу для заголовков и создает оглавление и вставляет его на страницу.

Поисковая система делает базу знаний гораздо более полезной, Wiki-Bot делает Wiki более доступным.