Статьи

Построение полнотекстового индекса коммитов git с использованием API lunr.js и Github

У Github есть хороший API для проверки репозиториев — он позволяет вам читать суть, проблемы, историю коммитов, файлы и так далее. Данные репозитория Git позволяют продемонстрировать мощь сочетания полнотекстового и граненого поиска, так как существует сочетание полей свободного текста (сообщения коммитов, код) и перечислимых полей (коммиттеры, даты, работодатели коммиттеров). API Github возвращают JSON, у которого есть приятное свойство напоминать древовидную структуру — результаты можно повторять, не опасаясь бесконечных циклов. Обратите внимание: чтобы загрузить всю историю коммитов для репозитория, вам нужно пролистать ее с помощью ша хэша. API, который я здесь использую, не имеет различий, которые должны быть найдены в другом месте.

Чтобы проверить это, перейдите по URL-адресу следующим образом. Настраиваемые аргументы — это поля владельца хранилища и имени.
https://api.github.com/repos/torvalds/linux/commits

Вот как выглядит коммит:

{
  "sha": "7638417db6d59f3c431d3e1f261cc637155684cd",
  "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/7638417db6d59f3c431d3e1f261cc637155684cd",
  "author": {
    "date": "2008-07-09T16:13:30+12:00",
    "name": "Scott Chacon",
    "email": "[email protected]"
  },
  "committer": {
    "date": "2008-07-09T16:13:30+12:00",
    "name": "Scott Chacon",
    "email": "[email protected]"
  },
  "message": "my commit message",
  "tree": {
    "url": "https://api.github.com/repos/octocat/Hello-World/git/trees/827efc6d56897b048c772eb4087f854f46256132",
    "sha": "827efc6d56897b048c772eb4087f854f46256132"
  },
  "parents": [
    {
      "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/7d1b31e74ee336d15cbd21741bc88a537ed063a0",
      "sha": "7d1b31e74ee336d15cbd21741bc88a537ed063a0"
    }
  ]
}

Чтобы сделать тест простым, я загружаю их как JSON локально, затем запускаю веб-сервер на python. Если бы я делал много таких звонков на общедоступном сайте, я бы настроил прокси для API github.

python -m SimpleHTTPServer

Эти данные содержат несколько вложенных объектов и должны быть сведены в соответствии с  полнотекстовым индексом lunr.js. В этом примере в качестве местоположения в индексе используется номер фиксации (0, 1, 2..N), но в реальной среде следует использовать хэш фиксации, чтобы разрешить разбиение процесса загрузки. Вложенные объекты сглаживаются путем объединения последующих клавиш с подчеркиваниями между ними. Чтобы избежать столкновений, необходимо избежать производственного решения.

var documents = [];
 
function recurse(doc_num, base, obj, value) {
  if ($.isPlainObject(value)) {
    $.each(value, function (k, v) {
      recurse(doc_num, base + obj + "_", k, v);
    });
  } else {
    process(doc_num, base + obj, value);
  }
}
 
function process(doc_num, key, value) {
  if (documents.length <= doc_num)
    documents[doc_num] = {};
 
  if (value !== null)
    documents[doc_num][key] = value + '';
}
 
$.each(data, function(doc_num, commit) {
  $.each(commit, function(k, v) {
    recurse(doc_num, '', k, v)
  });
});

Обычно настраивают полнотекстовый индекс лунра, указав все поля, во многом как XML-файлы конфигурации Solr. У Lunr не так много опций конфигурации, так как вы указываете только параметр ‘boost’, чтобы увеличить значение определенных полей в ранжировании. Я предполагаю, что это изменится с ростом проекта, по крайней мере, для включения подсказок типа.

Учитывая простоту объектов полей, вы можете вывести список полей из полезных нагрузок JSON. Приведенный ниже код предоставляет два режима: один, где вы проверяете всю полезную нагрузку JSON, или другой, где вы ограничиваете количество проверок коммитов, хороший вариант, когда данные JSON согласованы.

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

function inferIndex(documents, config) {
  return lunr(function() {
    this.ref('id');
    var found = {};
    var idx = this;
 
    $.each(documents,
      function(doc_num, doc) {
 
        if (config &&
            config.limit &&
            config.limit < doc_num)
          return;
 
        $.each(doc, function(k, v) {
          if (!found[k]) {
            if (config && config[k]) {
              idx.field(k, config[k]);
            } else {
              idx.field(k);
            }
            found[k] = true;
          }
        });
    });
  });
}
 
var index =
  inferIndex(documents,
    {limit: 1,
     'commit_author_name':{boost:10}});

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

$.each(documents,
  function(doc_num, attrs, doc_cb) {
    var doc =
      $.extend(
        {id: doc_num}, attrs);
 
    if (doc_cb) {
      doc = doc_cb(doc);
    }
 
    index.add(doc);
});

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

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

function facet(index, query, data, field) {
  var results = index.search(query);
 
  var facets = {};
  $.each(results, function(index, searchResult) {
    var doc = data[searchResult.ref];
 
    facets[doc[field]] =
      (facets[doc[field]] === undefined ? 0 :
      facets[doc[field]]) + 1; } );
 
  return facets;
}

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

Следующий запрос демонстрирует эту технику:

var facets =
   facet(index,
        'driver',
        documents,
        'commit_author_name');
{"Wolfram Sang":24,"Linus Torvalds":3}

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