Skip to content

Веб-интерфейс к поисковику

20/04/2010

Итак, почти всё готово для использования поисковика. Паук собирает данные в CouchDB, xmlpipe2 передаёт их Sphinx, Limestone может делать по ним поиск с помощью Sphinx. Осталось обернуть всё это в удобный для использования сервис.

Доработки паука и индексатора

Главная проблема на этом этапе: несовпадение формата ID у Sphinx и формата по умолчанию в CouchDB. При получении массива результатов Sphinx мы получаем только обычные целые числа (32-bit integer), а документам в CouchDB назначены строковые ID. Впрочем, решить проблему можно довольно просто: CouchDB позволяет сохранять документы с произвольно заданными ID, так что мы просто переделаем паука чтобы он назначал странице числовой id и сохранял её с этим идентификатором.

Также тестовый прогон на других сайтах показал что есть проблема с относительными ссылками вроде ./viewtopic.php?id=12. В node такие ссылки можно разворачивать относительно текущего URL с помощью соответствующего модуля:

    var sys = require('sys'),
        url = require('url');

    var base_url = 'http://example.com/forum/viewtopic.php?id=1';
    var relative_url = './viewtopic.php?id=2';

    var link = url.resolve(base_url, relative_url);

    sys.puts(link); // http://example.com/forum/viewtopic.php?id=2
    

Общая архитектура

Сам сайт очень прост, он будет состоять всего из двух страниц. На первой располагается форма поиска, вторая отображает результаты. Первая страница отсылает строку поиска в GET-запросе, на манер Google. Вторая страница делает поиск в Sphinx, получает id соответствующих запросу страниц и вытягивает из CouchDB их названия и адреса.

Для реализации этого сервиса я взял фреймворк Express версии 0.9.0. Кроме него мне понадобятся коннекторы limestone и node-couch, и фреймворк Do в качестве комбо-библиотеки.

Форма поиска

Вначале делаем каркас сайта в Express:

var kiwi = require('kiwi');

kiwi.require('express');

configure(function() {
    set('root', __dirname);
});

get('/', function(){
    return 'Home page'
});

get('/search', function(){
  return 'Search results for ' + this.param('query');

});

run(8000, '192.168.0.200');
    

Метод this.param('query') возвращает GET-параметр, переданный странице. Итак, на главной странице нам нужна форма. Проще всего не выводить её с помощью return, а воспользоваться встроенным в Express шаблонизатором на основе HAML+SASS. Создаём главный HAML-шаблон — он должен лежать в папке views и называться layout.html.haml:

%html
  %head
    %title= 'Search'
    %script{ src: '/files/javascripts/jquery.js' }
    %script{ src: '/files/javascripts/app.js' }
    %link{ rel: 'stylesheet', href: '/files/style.css' }
  %body
    #wrapper
      != body
    

(К сожалению, WP.com не подсвечивает HAML). Код взят прямиком из примеров к фреймоворку. Здесь мы можем подключать нужные CSS-файлы и библиотеки.

Теперь сделаем саму форму. Файл будет называться home.html.haml (впрочем, он может называться как угодно):

%h1 Search

%form{ method: 'get', action: 'search' }
  %input{ type: 'text', name: 'query', value: 'Search text' }
  %input{ type: 'submit', value: 'Search!' }
    

Он тоже отправляется в папку views. Теперь используем его:

var kiwi = require('kiwi');

kiwi.require('express');

configure(function() {
    set('root', __dirname);
});

get('/', function(){
    this.render('home.html.haml');
});

get('/search', function(){
  return 'Search results for ' + this.param('query');

});

run(8000, '192.168.0.200');
    

Теперь попробуем это запустить. По указанному адресу Вы должны увидеть страницу с формой.

Страница результатов

Страница результатов получает строку запроса через this.param('query'). Теперь нам надо передать эту строку в Limestone, чтобы получить массив найденных страниц:


get('/search', function() {
  var self = this;
  limestone.connect(9312, function(err) {
    if (err) {
        return 'Connection error';
    }
    limestone.query({'query': query, maxmatches: 20}, function(err, answer) {
      limestone.disconnect();
      return 'Search results for ' + this.param('query') + ': ' + JSON.serialize(answer);
    });
  });

});
    

Страница будет показывать объект результатов в виде JSON. Уже неплохо.

Теперь нам надо достать каждый результат из CouchDB. Sphinx возвращает нам только ID документов, поэтому заголовок, ссылку и другие вещи придётся забирать самому.

Само получение документа из CouchDB выглядит просто:

db.openDoc('23', {
    'success': function(page) {
        sys.puts('Got doc ' + match.doc + ': ' + JSON.stringify(page));
        //callback(page);
    },
    'error': function(err) {
        //callback();
    }
});
    

Сложность в том, что у нас таких документов несколько. Нам нужно отправить запросы на получение документов, дождаться их всех и только потом отрисовывать страницу. Для этого нам и пригодится комбо-библиотека Do.

В Do есть удобная функция Do.map, принимающая на вход массив значений и асинхронную функцию, выполняющая асиинхронную функцию для всех элементов массива и возвращающая массив результатов. Как раз то, что нужно:

limestone.query({'query': query, maxmatches: 20}, function(err, answer) {
    limestone.disconnect();
    Do.map(answer.matches, function(match, callback) {

        db.openDoc(match.doc.toString(), {
            'success': function(page) {
                callback(page);
            },
            'error': function(err) {
                callback();
            }
        });

    })(function(elements){
        return 'Search results for ' + this.param('query') + ': ' + JSON.serialize(elements);
    });
});
    

Теперь у нас есть не просто массив ID страниц, а все необходимые данные. Осталось сделать шаблон для результатов поиска. Во-первых, сама страница результатов (results.haml.html):

%h1 Search results

%form{ method: 'get', action: 'search' }
  %input{ type: 'text', name: 'query', value: query }
  %input{ type: 'submit', value: 'Search!' }

%matches= number_of_matches

%ul#results
  != this.partial('result.html.haml', {collection: results})
    

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

%li%a{href:result.url}= result.title
    

Collection: results означает что мы передаём в шаблон массив, и надо пройти по его элементам. Данные в шаблон передаются с помощью второго параметра к this.render: свойство locals будет разобрано на переменные:

self.render('results.html.haml', {
    'locals': {
        'header': 'Search results for "' + query + '"',
        'query': query,
        'number_of_matches': 'Found ' + answer.match_count + ' matches',
        'results': pages
    }
});
    

Массив pages — просто немного обрезанный результат Do.map, из которого выкинуты лишние данные вроде полного текста страниц. Впоследствии можно допилить limestone для возврата отрывков страницы, в которых найдены слова.

Страница с результатами поиска в Chrome:

Для проверки паука я индексировал сайт Final Fantasy Wiki, поэтому результаты немного однообразны🙂

Создание поискового сервера

Ссылки по теме

Добавить комментарий

Добавить комментарий

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

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: