Thu. Dec 5th, 2024

Búsqueda estática en sitios con Hugo + App Engine + API de búsqueda + Python


Cuando el año cambió a 2018, decidí abandonar WordPressque había estado usando durante más de 12 años como mi sistema de gestión de contenido de elección. Tenía muchas razones para hacerlo, pero las motivaciones más importantes eran la oportunidad de probar algo nuevo y abandonar la hinchazón y el desorden de WordPress por un orden de cosas más easy y elegante. Alentado por otro adoptante, Marcos EdmondsonDecidí dar Hugo un ir (juego de palabras intencionado).

La migración no fue fácil, ya que tuve que convertir la base de datos relacional de WordPress en una lista estática de archivos Markdown. En el proceso, tuve que configurar dos temas (common y AMPERIO), optimizo todas mis imágenes, JavaScript y hojas de estilo, y reviso cada uno de mis más de 200 artículos en busca de problemas de estilo y enlaces rotos (¡madre mía, había muchos de esos!).

Hugo está escrito en el Ir Idioma, y ​​es bastante fácil de usar si estás familiarizado con él. reducción y la línea de comandos de su sistema operativo. El truco de un sitio estático es que todo el contenido se almacena en archivos estáticos en su servidor de archivos. No hay una base de datos relacional a la que recurrir, lo que significa que un sitio estático puede ser increíblemente rápido y una tarea ardua de mantener.

Uno de los mayores dolores de cabeza para mí fue cómo configurar la búsqueda en el sitio. Sin una base de datos o un servidor internet que genere documentos HTML dinámicos, encontrar una forma adecuada de indexar el contenido en el navegador y responder de manera rápida y eficiente a las consultas de búsqueda parecía una tarea insuperable.

Al principio probé varias cosas, entre ellas:

  • Algoliaal que tuve que renunciar porque tenía demasiado contenido para su nivel gratuito.

  • lunr.js ejecutándose en una máquina digital NodeJS en la nube de Google, a la que tuve que renunciar porque recibí una factura de 400$ por ejemplo, solo por el mantenimiento del mes de diciembre.

  • Solución personalizada que digirió JSON generado por Hugo y lo analizó para buscar con jQuery directamente en el navegador, algo a lo que tuve que renunciar ya que descargar un archivo JSON indexado de alrededor de 5 megabytes en cada página no favorece una buena experiencia de usuario.

Después del experimento fallido con lunr.js, todavía quería darle otra oportunidad a App Engine de Google. Estoy enamorado de App Engine desde que publiqué la primera versión de mi Herramientas GTM Bueno, resulta que App Engine tiene una herramienta muy útil y versatile. API de búsqueda para Python, que parece estar hecho a medida para funcionar con el JSON generado por Hugo en un sitio estático.


incógnita


El boletín informativo de Simmer

Suscríbete a la Boletín informativo de Simmer ¡Para recibir las últimas noticias y contenidos de Simo Ahava en tu bandeja de entrada de correo electrónico!

La configuración

Mi configuración se ve así:

  1. El Archivo de configuración de Hugo está configurado para generar una index.json en el directorio público, con todo el contenido de mi sitio listo para indexar.

  2. Un script que implementa este archivo JSON en el proyecto de App Engine.

  3. Un proyecto de App Engine que utiliza el API de búsqueda de Python cliente para construir un índice de este JSON.

  4. El proyecto de App Engine también proporciona un punto closing HTTP al que mi sitio realiza todas las consultas de búsqueda. Cada solicitud se procesa como una consulta de búsqueda y el resultado se devuelve en la respuesta HTTP.

  5. Finalmente, tengo un montón de JavaScript que ejecuta el formulario de búsqueda y la página de resultados de búsqueda en mi sitio, enviando la solicitud al punto closing de App Engine y formateando la página de resultados de búsqueda con la respuesta.

La belleza de usar la API de búsqueda es que estoy muy por debajo de la límites de cuota para la versión gratuita, ¡y así no tengo que pagar ni un centavo para que todo funcione!

1. La modificación del archivo de configuración

El cambio en el archivo de configuración de Hugo es fácil de realizar, ya que Hugo tiene soporte integrado para generar JSON en un formato que la mayoría de las bibliotecas de búsqueda digieren. En el archivo de configuración, debe encontrar el output configuración y agregar "JSON" como una de las salidas para el residence Tipo de contenido. Se parece a esto:

(output)
  residence = ( "HTML", "RSS", "JSON" )

Este cambio de configuración genera un index.json archivo en la raíz de su carpeta pública cada vez que se crea el proyecto Hugo.

A continuación se muestra un ejemplo de cómo podría verse una publicación de weblog en este archivo:

(
    {
        "uri": "https://www.simoahava.com/upcoming-talks/",
        "title": "Upcoming Talks",
        "tags": (),
        "description": "My upcoming convention talks and occasions",
        "content material": "17 March 2018: MeasureCamp London 20 March 2018: SMX München 19 April 2018: Superior GTM Workshop (Hamburg) 24 Could 2018: NXT Nordic (Oslo) 20 September 2018: Superior GTM Workshop (Hamburg) 14-16 November 2018: SMXL Milan    I take pleasure in presenting at conferences and meetups, and I've a monitor document of tons of of talks since 2013, comprising keynotes, convention shows, workshops, seminars, and public trainings. Viewers sizes have various between 3 and a pair of,000.nMy favourite matters revolve round internet analytics improvement and analytics customization, however Iu0026rsquo;m very happy to speak about integrating analytics into organizations, data switch, bettering technical abilities, digital advertising, and content material creation.nSome of my convention slides could be discovered at SlideShare.nFor a pattern, hereu0026rsquo;s a chat I gave at Reaktor Breakpoint in 2015.n   You may contact me at simo (at) simoahava.com for enquiring about my availability on your occasion.n"
    }
)

2. El script de implementación

El script de implementación es un fragmento de código Bash que crea el sitio Hugo, copia el index.json en mi carpeta de proyecto de búsqueda y luego implementa todo el proyecto de búsqueda en App Engine. Así es como se ve:

cd ~/Paperwork/Tasks/www-simoahava-com/
rm -rf public
hugo
cp public/index.json ../www-simoahava-com-search/
rm -rf public
cd ~/Paperwork/Tasks/www-simoahava-com-search/
gcloud app deploy
curl https://search-www-simoahava-com.appspot.com/replace

El hugo El comando crea el sitio y genera la carpeta pública. Desde la carpeta pública, el index.json Luego se copia a mi carpeta de proyecto de búsqueda, que posteriormente se implementa en App Engine mediante el comando gcloud app deploy. Finalmente, un curl El comando a mi punto closing personalizado garantiza que mi secuencia de comandos de Python actualice el índice de búsqueda con la última versión de index.json.

3. El código Python ejecutándose en App Engine

En Motor de aplicacionesSimplemente creé un nuevo proyecto con un nombre que es fácil de recordar como punto closing. No agregué ninguna facturación a la cuenta, porque me propuse el desafío de crear una API de búsqueda gratuita para mi sitio.

Ver Esta documentación para obtener una guía de inicio rápida sobre cómo comenzar a usar Python y App Engine. Concéntrese especialmente en cómo configurar el proyecto de App Engine (no necesita habilitar la facturación) y cómo instalar y configurar el gcloud Herramientas de línea de comandos para su proyecto.

El código Python se ve así.

#!/usr/bin/python

from urlparse import urlparse
from urlparse import parse_qs

import json
import re

import webapp2
from webapp2_extras import jinja2

from google.appengine.api import search

# Index identify on your search paperwork
_INDEX_NAME = 'search-www-simoahava-com'


def create_document(title, uri, description, tags, content material):
    """Create a search doc with ID generated from the submit title"""
    doc_id = re.sub('(s+)', '', title)
    doc = search.Doc(
        doc_id=doc_id,
        fields=(
            search.TextField(identify='title', worth=title),
            search.TextField(identify='uri', worth=uri),
            search.TextField(identify='description', worth=description),
            search.TextField(identify='tags', worth=json.dumps(tags)),
            search.TextField(identify='content material', worth=content material)
        )
    )
    return doc


def add_document_to_index(doc):
    index = search.Index(_INDEX_NAME)
    index.put(doc)
	

class BaseHandler(webapp2.RequestHandler):
    """The opposite handlers inherit from this class. Supplies some helper strategies
    for rendering a template."""

    @webapp2.cached_property
    def jinja2(self):
        return jinja2.get_jinja2(app=self.app)


class ProcessQuery(BaseHandler):
    """Handles search requests for feedback."""

    def get(self):
        """Handles a get request with a question."""
        uri = urlparse(self.request.uri)
        question = ''
        if uri.question:
            question = parse_qs(uri.question)
            question = question('q')(0)

        index = search.Index(_INDEX_NAME)

        compiled_query = search.Question(
            query_string=json.dumps(question),
            choices=search.QueryOptions(
                sort_options=search.SortOptions(match_scorer=search.MatchScorer()),
                restrict=1000,
                returned_fields=('title', 'uri', 'description')
            )
        )
		
		outcomes = index.search(compiled_query)

        json_results = {
            'outcomes': (),
            'question': json.dumps(question)
        }

        for doc in outcomes.outcomes:
            search_result = {}
            for area in doc.fields:
                search_result(area.identify) = area.worth
            json_results('outcomes').append(search_result)
        self.response.headers.add('Entry-Management-Permit-Origin', 'https://www.simoahava.com')
        self.response.write(json.dumps(json_results))
		

class UpdateIndex(BaseHandler):
    """Updates the index utilizing index.json"""

    def get(self):
        with open('index.json') as json_file:
            knowledge = json.load(json_file)

        for submit in knowledge:
            title = submit.get('title', '')
            uri = submit.get('uri', '')
            description = submit.get('description', '')
            tags = submit.get('tags', ())
            content material = submit.get('content material', '')

            doc = create_document(title, uri, description, tags, content material)
            add_document_to_index(doc)
			

software = webapp2.WSGIApplication(
    (('/', ProcessQuery),
     ('/replace', UpdateIndex)),
    debug=True)

Al closing, estoy vinculando las solicitudes a la / punto closing a ProcessQueryy solicita a /replace a UpdateIndexEn otras palabras, estos son los dos puntos finales a los que estoy sirviendo.

UpdateIndex carga el index.json archivo, y para cada pieza de contenido dentro de él (publicaciones de weblog, páginas, and so forth.), captura el title, uri, description, tagsy content material parámetros del contenido JSON y crea documentos para cada instancia. Luego, cada documento se agrega al índice.

Así es como se puede utilizar la API de búsqueda para traducir cualquier archivo JSON en un índice de búsqueda válido, sobre el cual luego se pueden crear consultas.

Las consultas se realizan mediante sondeo /?q= punto closing, donde key phrase coincide con una consulta válida contra la API de búsqueda motor de consultaCada consulta es procesada por ProcessQueryque toma el término de consulta, sondea el índice de búsqueda con ese término y luego compila un resultado de todos los documentos que el índice de búsqueda devuelve para esa consulta (en orden de clasificación). Luego, este resultado se envía al cliente en una respuesta JSON.

La API de búsqueda te da mucho margen para optimizar el índice y compilar consultas complejas. He optado por un enfoque bastante mundano, que puede dar lugar a algunos valores atípicos de clasificación extraños, como documentos que deberían estar claramente en la parte superior de una lista de resultados relevantes que terminan en la parte superior, pero aún así estoy bastante satisfecho con la solidez de la API.

4. El JavaScript

Por último, necesito algo de código del lado del cliente para producir la página de resultados de búsqueda. Dado que Hugo no tiene un servidor internet, no puedo realizar la búsqueda del lado del servidor; debe hacerse en el cliente. Este es uno de los casos en los que un sitio estático pierde algo de su brillo en comparación con sus contrapartes que vienen equipadas con un servidor internet y capacidades de procesamiento del lado del servidor. Un sitio Hugo se crea y se implementa de una sola vez, por lo que no hay una generación dinámica de páginas HTML después de la creación; todo tiene que suceder en el cliente.

De todos modos, el formulario de búsqueda de mi sitio es muy easy. Se parece a esto:

<type id="search" motion="/search/">
  <enter identify="q" sort="textual content" class="form-control input--xlarge" placeholder="Search weblog..." autocomplete="off">
type>

Cuando se envía el formulario, se realiza una solicitud GET al /search/ página en mi sitio, agregando lo que se escribió en el campo como q parámetro de consulta, por lo que la URL se convierte en algo como

https://www.simoahava.com/search/?q=google+tag+supervisor

En el /search/ Página, tengo un indicador de carga que espera hasta que se full la solicitud al punto closing de búsqueda. La llamada de búsqueda se realiza con JavaScript y se ve así:

(perform($) {

    var printSearchResults = perform(outcomes) {
	  // Replace the web page DOM with the search outcomes...
	};

    var endpoint = 'https://search-www-simoahava-com.appspot.com';

    var getQuery = perform() {
        if (window.location.search.size === 0 || !/(?|&)q=/.check(window.location.search)) {
            return undefined;
        }

        var components = window.location.search.substring(1).cut up('&');
        var question = components.map(perform(half) {
            var temp = half.cut up('=');
            return temp(0) === 'q' ? temp(1) : false;
        });
        return question(0) || undefined;
    };

    $(doc).prepared(perform() {
        var question = getQuery();

        if (typeof question === 'undefined') {
            printSearchResults();
            return;
        } else {
            $.get(endpoint + '?q=' + question, perform(knowledge) {
                printSearchResults(JSON.parse(knowledge));
            });
        }
    });
	
})(window.jQuery)

Para simplificar las cosas, solo he incluido los fragmentos de código relevantes que también se pueden usar en otros lugares. En resumen, cuando el /search/ La página se carga, cualquiera que sea el valor incluido en la q El parámetro de consulta se envía inmediatamente al punto closing de la API de búsqueda. Luego, la respuesta se procesa y se incorpora a una página de resultados de búsqueda.

Entonces, si la URL de la página es https://www.simoahava.com/search/?q=google+tag.supervisorEste fragmento de JavaScript lo convierte en una solicitud GET a https://search-www-simoahava-com.appspot.com/?q=google+tag+supervisorPuedes visitar esa URL para ver cómo se ve la respuesta.

Esta respuesta se procesa y se crea la página de resultados de búsqueda.

Resumen

Así es como elegí construir la búsqueda del sitio utilizando la flexibilidad de Hugo junto con la potente API de búsqueda que ofrece App Engine de Google.

Según mi limitada cantidad de investigación, es una solución tan buena como cualquier otra y parece bastante rápida sin comprometer la potencia del motor de búsqueda. Sin embargo, a medida que se acumula más contenido, es posible que el motor de búsqueda se vuelva más lento o que comience a alcanzar mis cuotas de nivel gratuito, en cuyo caso tendré que replantear mi enfoque.

El punto débil en este momento es que todo se hace del lado del cliente. Eso significa que, contrariamente a la filosofía de los sitios estáticos, gran parte del procesamiento se realiza en el navegador. Pero no estoy seguro de cómo se podría evitar esto, ya que un sitio estático no ofrece las capacidades de un procesador del lado del servidor.

En este momento, es un compromiso que estoy dispuesto a hacer, pero estoy ansioso por escuchar comentarios si la búsqueda es torpe o no funciona correctamente para usted.

Related Post

Leave a Reply

Your email address will not be published. Required fields are marked *