Modelagem Do Sistema

Aspectos técnicos e principais módulos do sistema bem como o código utilizado em algumas implemtentações adotadas.

Modularização do Metabus

A modularização dos arquivos e diretórios é extremamente importante, principalmente para permitir a escalabilidade do sistema. Os projetos e módulos foram separados a fim de reutilizar o código e facilitar a manutembilidade e o entendimento do todo, permitindo assim a expansão organizada do projeto.

O repositório está dividido em dois projetos mbclient - projeto da interface gráfica do sistema - e mbserver - projeto dos servidores responsável por centralizar e requisitar paralelamente a busca nas fontes selecionadas. O papel deses projetos e seus modulos serão detalhados abaixo.

MBSERVER

Este é o projeto principal do metabus. Ao receber a requisição de busca em um grupo de fontes divide essas tarefas entre os servidores de busca disponíveis. Um servidor de busca é responsável por processar e requisitar paralelamente os resultados da pesquisa em cada fonte. Para isso as requisições são adicionadas em filas de prioridade de acordo com o seu estado no cache, de forma a dar maior prioridade às pesquisas que ainda não foram cacheadas.

Antes de adicionar os resultados no sistemas de cache verifica-se o formato da resposta, caso não seja RSS são aplicadas as transformações XSLT necessárias para salvar os dados da busca.

SERVER

Esta aplicação é o módulo responsável por pesquisar pelas palavras-chave da busca do usuário em todas as fontes cadastradas. Assim que um resultado é obtido, são aplicados parsers nesse resultado a fim de extrair as informações relevantes do resultado da busca.

O servidor possui um parser de RSS somente. Dessa forma, sempre que a fonte de busca não disponibiliza os resultados em RSS uma etapa adicional de parsing processa o resultado por meio de uma transformação XSLT, que está associada a uma fonte. Essa etapa visa transformar um documento XHTML em RSS, para que o servidor possa interpretar os resultados.

Para de obtenção de resultados das fontes de busca, o servidor mantém um pool de threads para efetuarem as requisições HTTP para as fontes. O número de Threads de cada processo deve ser configurável baseado em testes com fontes reais, com o objetivo de atingir uma configuração ótima para cada caso. O tempo médio que cada thread fica bloqueada e o número médio de fontes processadas por unidade de tempo, são os principais fatores parâmetros para mensurar o número necessário de threads.

Abaixo segue um trecho do código desse pool de threads retirado do arquivo pool.py:

class MBThreadPool(Singleton):
    """
        This class must be Singleton, becouse it's suppose to exist only one
        of this pool per process. The total number of threads will be 
        the number of process x number of MBThreadPool threads.
    """
    __pool = None

    def __init__ (self):
        if MBThreadPool.__pool is None:
            self.ioworkers = []
            self.parser = None
            self.requests = Queue(0)
            self.bg_requests = Queue(0)
            self.responses = Queue(0)

            for proc in range(settings.THREADS_NUMBER):
                w = WorkerThread(self.requests, self.bg_requests, self.responses)
                w.start()
                self.ioworkers.append(w)

            self.parser = ParserThread(self.responses)
            self.parser.start()

Todas as requisições a uma fonte são inseridas na fila requests do pool. As threads ficam verificando essa fila a procura de requisições, sempre que uma thread encontra uma requisição ela a retira da fila e efetua esta requisição. Quando essa thread termina de obter o resultado da busca ela insere o documento resultante da busca em outra fila chamada responses, da mesma forma existe uma thread que observa essa fila a procura de documentos obtidos quando um documento é encontrado ele entra na fase de parsing, onde os dados são extraídos desses documentos e armazenados no cache.

SERVER_LIB

A arquitetura do metabus permite que vários servidores sejam cadastrados e utilizados para realizar parte do processamento das buscas. Sendo assim, tornou-se necessário centralizar a informação do cadastro de servidores. O módulo server_lib além de possuir tal cadastro, ainda é responsável pelo balanceamento de carga entre eles, distribuindo as pesquisas de maneira uniforme para todos os servidores cadastrados. Uma vez que, a comunicação entre módulo de interface gráfica e o servidor é feita exclusivamente através do server_lib.

Segue o código da função que realiza o acionamento do servidor para efetuar as buscas necessárias:

def do_search(self):
        """
            Metabus server works as an asynchronous server. It's only possible 
            to retrive any results after the timeout.
        """

        values = {
                    'sources' : simplejson.dumps(self.source_array),
                    'keyword' : self.keywords.encode('utf8'),
                 }

        # Get a server in a RR queue
        server = Server()
        url = server.get() + 'server/process/'

        data = urllib.urlencode(values)
        req = urllib2.Request(url, data)
        response = urllib2.urlopen(req)

        self.started = True
        self.start_time = time.time()

CACHE

Toda vez que um usuário realiza uma busca o servidor faz cálculos, requisições para outras páginas e consultas na base de dados para montar o resultado da pesquisa. Para a maioria dos sites o processamento do servidor não é um gargalo, entrentando quando pretende-se atender a um grande número de buscas o número de requisições pode ser muito alto pois cada requisição de busca feita pelo usuário pode consultar diversas fontes em apenas uma pesquisa.

Para melhorar o desempenho e evitar que pesquisas iguais sejam requisitadas mais de uma vez em um curto espaço de tempo um mecanismo de cache foi introduzido no sistema a fim de salvar resultados de buscas recentes.

A estrutura dos dados do cache tem objetivo de melhorar a performance, um vez que buscas recentes são retornadas com uma simples busca no banco de dados.

O código a seguir mostra a interface das principais funções do cache do metabus baseado no sistema de cache do próprio Django:

class BaseCache(object):
    def add(self, key, value, timeout=None):
        """
        Set a value in the cache if the key does not already exist. If
        timeout is given, that timeout will be used for the key; otherwise
        the default cache timeout will be used.

        Returns True if the value was stored, False otherwise.
        """
        raise NotImplementedError

    def get(self, key, default=None):
        """
        Fetch a given key from the cache. If the key does not exist, return
        default, which itself defaults to None.
        """
        raise NotImplementedError

    def has_key(self, key):
        """
        Returns True if the key is in the cache and has not expired.
        """
        return self.get(key) is not None

O módulo de cache do metabus implementa as funções de cache definidas acima. A remoção das pesquisas salvas é feita no momento em que se obtem uma entrada do cache, se a pesquisa estiver expirada ela é removida da base de dados, como mostra o código abaixo:

    def get(self, key, default=None):
        try:
            key_row = Key.objects.filter(source=key["source"],keyword=key["keyword"]).get()
        except Key.DoesNotExist:
            return default
        else:
            now = datetime.now()
            if key_row.expires < now:
                Value.objects.filter(key=key_row).delete()
                key_row.delete()
                return default
            return Value.objects.filter(key=key_row).all()

SOURCE

Este módulo do sistema provê as definições do modelo de dados utilizados de maneira geral, sem se preocupar com definições de tipos de dados e Sistemas de Gerenciamento de Bancos de Dados - SGBD. O modelo de dados das fontes é ilustrado na Figura abaixo junto com o modelo do cache.

-foto-

O modelo de dados das fontes foi concebido baseado nos seguintes requisitos:

  • Cadastrar fontes por meio de urls de buscadores web, definindo no cadastro as configurações básicas como timeout, operadores, tempo de permanência no cache, etc.
  • Possibilitar o agrupamento de Fontes em Grupos, com o objetivo de facilitar a escolha das fontes a serem pesquisadas. Os Grupos de fontes poderão possuir sub-grupos para melhor organizar as fontes, facilitando a seleção de fontes que uma busca contemplará8.
  • A cada fonte deve estar associado um parser se esta fonte não retornar os resultados em formato RSS. Este parser terá um arquivo XSLT que descreve as transformações necessárias para se obter um documento RSS a partir do documento XHTML retornado pela pesquisa;

MBCLIENT

Como foi dito, a interface gráfica está separada da implementação do servidor para tornar mais fácil a implementação de uma outra interface. O layout do metabus é semelhante aos buscadores existentes e contém elementos simples e claros para a realização das buscas.

Independente da interface implementada deverá ser fornecido um formulário, para o usuário requisitar a busca ao servidor que retornará os resultados da pesquisa (título, descrição, link e fonte dos resultados encontrados). O código abaixo apresenta o formulario de busca gerado pelo template do metabus:

<form action="/search" method="get" id="search-form">
    <fieldset>
        <input type="text" name="keywords" id="keywords"/>
        <input type="submit" value="search"/>
    </fieldset>
    <fieldset id="sources">
        <ul>
            <li>
                <input type="checkbox" name="groups" value="1" id="Rio de Janeiro" class="groups"/>
                <label for="Rio de Janeiro" class="groups">Rio de Janeiro</label>
                <fieldset class="groups">
                    <ul>
                        <li>
                            <input type="checkbox" name="sources" value="7" id="Prefeitura Municipal do Buzios"/>
                            <label for="Prefeitura Municipal do Buzios">Prefeitura Municipal do Buzios</label>
                        </li>
                        <li>
                            <input type="checkbox" name="sources" value="6" id="Prefeitura Municipal do Rio de Janeiro"/>
                            <label for="Prefeitura Municipal do Rio de Janeiro">Prefeitura Municipal do Rio de Janeiro</label>
                        </li>
                    </ul>
                </fieldset>
            </li>
            <li>
                <input type="checkbox" name="groups" value="2" id="São Paulo" class="groups"/>
                <label for="São Paulo" class="groups">São Paulo</label>
                <fieldset class="groups">
                    <ul>
                        <li>
                            <input type="checkbox" name="sources" value="4" id="Prefeitura Municipal de Diadema"/>
                            <label for="Prefeitura Municipal de Diadema">Prefeitura Municipal de Diadema</label>
                        </li>
                        <li>
                            <input type="checkbox" name="sources" value="1" id="Prefeitura Municipal de Mogi Mirim"/>
                            <label for="Prefeitura Municipal de Mogi Mirim">Prefeitura Municipal de Mogi Mirim</label>
                        </li>
                        <li>
                            <input type="checkbox" name="sources" value="2" id="Prefeitura Municipal de São Paulo"/>
                            <label for="Prefeitura Municipal de São Paulo">Prefeitura Municipal de São Paulo</label>
                        </li>
                    </ul>
                </fieldset>
            </li>
        </ul>
    </fieldset>
</form>

Os dados dos formulários são enviados para uma view que faz uma requisição para o server_lib após validar os campos. O código abaixo mostra esta etapa da busca.

def search(request):
    params = get_search_params()
    error = False

    if 'keywords' in request.GET and len(request.GET['keywords']) > 0:
        params['search_keywords'] = request.GET['keywords']
    else:
        params['search_keywords_error']= _('Please type one keyword to search')
        error = True

    if 'sources' in request.GET and len(request.GET['sources']) > 0:
        params['search_sources'] = request.GET.getlist('sources')
    else:
        params['search_sources_error']= _('Please select one or more sources to search')
        error = True

    if error:
        t = loader.get_template('base.html')
        c = RequestContext(request, params)
        return HttpResponse(t.render(c))

    search = Search(params['search_keywords'], sources=params['search_sources'])
    search.do_search()
    params['results'] = search.get_results()

    t = loader.get_template('result.html')
    c = RequestContext(request, params)
    return HttpResponse(t.render(c))

Ao iniciar uma busca a chamada do_search aciona o mecanismo de busca de cada uma das fontes selecionadas e adiciona os resultados no sistema de cache. O navegador, por sua vez, aguarda o tempo da busca. Logo após, monta o resultado a partir das informações guardadas no cache. Caso uma busca não tenha obtido o resultado dentro do tempo estipulado pelo usuário o sistema continua a verificar o estado da busca nessas fontes a fim de atualizar o resultado da pesquisa.

Última modificação 4 anos atrás Última modificação em 21/03/2013 16:25:49
 

The contents and data of this website are published under license:
Creative Commons 4.0 Brasil - Atribuir Fonte - Compartilhar Igual.