Оперируем двоичными данными в Node.js. Часть 2: получение ответа
В предыдущей статье мы собрали и отправили серверу запрос, и получили двоичную строку в ответ. Самое время эту строку препарировать 🙂
Разбираем ответ сервера
Структура ответа в Sphinx довольно проста, и понятна даже из кода PHP-коннектора (собственно, так я его и портировал). Сначала 8 байт заголовка: статус-код (int16), версия (int16), длина оставшейся части заголовка (int32). Последнее нам нужно разве что для проверки, т.к. tcp-модуль node всё равно забирает ответ Sphinx целиком и отдаёт его в callback уже склеенным.
Заголовок пакета
Вообще, разбор бинарных данных с bits.js
выглядит так же элегантно как сборка. Получаем заголовок:
var output = {}; var response = new bits.Decoder(data); var data_length = data.length; output.status = response.shift_int16(); output.version = response.shift_int16(); output.length = response.shift_int32();
Проверяем, что мы вообще получили:
if ( output.length != data.length - 8 ) { sys.puts("failed to read searchd response (status=" + output.status + ", ver=" + output.version + ", len=" + output.length + ", read=" + ( data.length - 8 ) + ")"); } if (output.version < search_command) { sys.puts("searchd command older than client's version, some options might not work"); } if (output.status == Sphinx.statusCode.WARNING) { sys.puts("WARNING: "); }
Когда мы делаем var response = new bits.Decoder(data), мы получаем копию двоичной строки для дальнейшей разделки. Так что надо отрезать уже разобранные 8 байт от оригинала и передать данные дальше.
Тело пакета
Разбор ответа я организовал про тому же принципу что и в PHP-коннекторе: первая функция отрезает и разбирает заголовок, проверяет целостность пакета и код статуса. Вторая парсит оставшуюся часть.
Пакет данных начинается опять-таки с кода статуса (теперь уже int32
). За ним идёт счётчик количества полей (я пока не понял зачем он нужен) и сами поля. Формат поля простой: длина названия (int32
) и само название. Разбираем их в цикле:
var output = {}; var response = new bits.Decoder(data); var i; output.status = response.shift_int32(); output.num_fields = response.shift_int32(); output.fields = []; output.attributes = []; output.matches = []; // Get fields for (i = 0; i < output.num_fields; i++) { var field = {}; field.length = response.shift_int32(); field.name = response.shift_raw_string(field.length); output.fields.push(field); }
Дальше идут атрибуты примерно в таком же виде, как поля. Счётчик, длина имени, имя, тип атрибута (int32
). Разбираем, складываем в массив:
output.num_attrs = response.shift_int32(); // Get attributes for (i = 0; i < output.num_attrs; i++) { var attribute = {}; attribute.length = response.shift_int32(); attribute.name = response.shift_raw_string(attribute.length); attribute.type = response.shift_int32(); output.attributes.push(attribute); }
Остались сами ответы и немного сопроводительной информации. Получаем счётчик ответов, потом сами ответы. Вместе с ответом приходит ID документа в виде int64
, но пока мы его отбросим. формат должен быть понятен из названий полей. Здесь используется список атрибутов, который мы получили ранее:
output.match_count = response.shift_int32(); output.id64 = response.shift_int32(); // Get matches for (i = 0; i < output.match_count; i++) { var match = {}; if (output.id64 == 1) { // here we must fetch int64 document id // and immediately throw half of it away )) var id64 = response.shift_int32(); match.doc = response.shift_int32(); match.weight = response.shift_int32(); } else { match.doc = response.shift_int32(); match.weight = response.shift_int32(); } var attrvals = []; match.attrs = {}; // var attr_value; for (attribute in output.attributes) { // BIGINT size attributes if (attribute.type == Sphinx.attribute.BIGINT) { attr_value = response.shift_int32(); attr_value = response.shift_int32(); match.attrs[attribute.name] = attr_value; continue; } // FLOAT size attributes (32 bits) if (attribute.type == Sphinx.attribute.FLOAT) { attr = response.shift_int32(); match.attrs[attribute.name] = attr_value; continue; } // We don't need this branch right now, // as it is covered by previous `if` // @todo: implement MULTI attribute type attr_value = response.shift_int32(); match.attrs[attribute.name] = attr_value; } output.matches.push(match); }
Id64
на второй строке — как раз флаг использования длинных ID для документов. Если он выставлен в 1, ID документа является 64-битным длинным целым, иначе — 32-битным integer.
В пакете теперь осталась только служебная информация вроде времени выполнения запроса. Её тоже берём.
output.total = response.shift_int32(); output.total_found = response.shift_int32(); output.msecs = response.shift_int32(); output.words = response.shift_int32();
Результат
Теперь можно вывести полученный объект output
через JSON.stringify()
в консоль, проверить что мы получили:
{ "status":0, "num_fields":2, "fields":[ {"length":5,"name":"title"}, { "length":7, "name":"content" }], "attributes":[ { "length":8, "name":"group_id", "type":1 }, { "length":10, "name":"date_added", "type":2 } ], "matches":[ { "doc":1, "weight":2, "attrs": { "undefined":1262017923 } }, { "doc":2, "weight":2, "attrs": { "undefined":1262017923 } }, { "doc":4, "weight":1, "attrs": { "undefined":1262017923 } } ], "num_attrs":2, "match_count":3, "id64":0, "total":3, "total_found":3, "msecs":2, "words":1 }
Это уже можно использовать. Как видно из дампа, 64-битные ID не использовались. Стало быть, мы получили настоящие id найденных документов: 1, 2 и 4.
Trackbacks & Pingbacks