Saltar a contenido

1.Introduccion ¿Que és?

TypeSense es un sofware Search Engine de codigo abierto que permite su instalacion on-premise aunque tambien ofrece su servicio SaaS).

Se caracteriza porque es un motor de busqueda ligero, de codigo abierto, soporta Typo-tolerance, voice-query, image-query y un largo etcetera.

Está muy enfocado al Retail donde este tipo de productos son el pilar.

2. Retail Focused

2.1 TypoTolerance

Nos ofrece una busqueda vectorial donde se optimiza las busqueda por similitud pero siendo muy potente con errores tipo graficos ya que se basa en algoritmos de LLM para determinar como crear los vectores afines.

Si bien una búqueda “%{like}%” nos permite detectar un patron para la búsqueda, ese valor del {like} aunque sea pequeño debe existir como tal y pertenecer a un valor de la tupla.

En TypeSense (mas enfocado a DB Document), la busqueda se hará por uno o varios campos de la entidad a buscar y ademas sera tolerante a errores tipograficos sin que nosotros tengamos que especificar nada en la busqueda.

2.2 Analytics Rules

Otro punto a favor, es que incluye reglas para eventos ocurridos en las colleciones de elementos.

En un buscador de retail son muy importantes los conceptos de popularidad y orden.

Nuestro sistema de busqueda, debe “estar vivo”, es decir nutrirse de los eventos que estan ocurriendo durante su funcionamiento.

De esta manera, nuestro buscador deberá mostrar los productos mas populares en las búsquedas , y esta información la debe sacar de los propios usuarios, por ejemplo un producto que se consulta muchas veces por los usuarios debería salir de los primeros en los listados, así como si el producto realiza una conversion (se vende) debería incrementar su popularidad mas que si solo es buscado.

Pues para todo este tipo de cosas TypeSense nos facilita el trabajo.

3. Características

A parte de las caracteristicas que hemos comentado antes, TypeSense nos ofrece:

  • Navegación facetada : sistema de busqueda por facetas
  • Geo-Search : Busqueda en base a GIS (Geographic Information System). Soporta Busqueda por Radio y tambien por forma poligonal especifica.
  • LLM Augmentation: Tecnologia basada en Large Language Model
  • Voice query: Busqueda por voz docs.
  • Image Query: Busqueda por imagen docs.
  • Federated MultiSeach: Permite busquedas en paralelo (distintas colecciones) con una unica petición.
  • JOIN Queries: Permite hacer busquedas filtradas en 2 (o más) colecciones. One-to-one, One-to-many, many-to-many, left joins… docs.
  • Synonyms: Permite definir sinonimos para los terminos de busquedas (por ejemplo si busco por “nike” internamente tambien buscará por calzado si lo definimos como sinonimo)
  • Alias: El uso de alias para las consultas nos permiten atacar a distintas versiones de una misma coleccion (si hubiera modificaciones en el formato o si hubiera una reindexacion de elementos) sin perdida de servicio.

In-Memory Search: Obviamente para la optimización y lograr un buen performance typeSense trabaja con toda la información en memoria (asi que necesitarás un buen host) que persiste a disco para tareas de backup y reinicios.

Permite por lo tanto una clusterización y sincronizacion de nodos que nos dará ese escalado horizontal que necesitaremos en entornos productivos.

4. Integración (clientes)

Tiene un montón de librerias cliente para un monton de lenguajes de programacion

Clientes

Tiene mucho trabajo hecho y tiene librerias de integracion web, que con pocas lineas de codigo nos montarán un buscador totalmente funcional de una manera extremadamente sencilla.

En la parte servidora ofrece clientes que hacen de wrapper para la integracion sencilla al API HTTP de los servidores typesense.

5. MONTANDO UN VIDEOCLUB

Vamos a montar un ejemplo rapido implementando un buscador para una tienda, vamos allá:

5.1 Infraestructura

Vamos a desplegar una imagen docker para jugar con el servidor.

Usamos este docker-compose.yaml:

version: '3.7'

networks:
  typesense-demo-network:
    name: typesense-demo-network

services:
  typesense:
    image: typesense/typesense:27.1
    ports:
      - "8108:8108"
    volumes:
      - ./typesense-data:/data
    command: '--data-dir /data --api-key=xyz --enable-cors --enable-search-analytics=true --analytics-dir=/analytics-data --analytics-flush-interval=60'
    networks:
      - typesense-demo-network

Es auto-explicativo, pero cabe mencionar que el parametro –enable-search-analytics=true es necesario establecerlo a true para poder usar las reglas analiticas que comenté en la seccion 2.2.

5.1.1 Infraestructura UI

Por defecto, los servidores de typsense no traen una adminsitración web, pero puedes usar esta utilidad para tener una administracion en local:

https://bfritscher.github.io/typesense-dashboard/#/

Al entrar en la web, indica tus datos para localhost que basta con indicar el apikey: “xyz”

typesense-login.png

Nos lleva al dashborad donde vemos todos los cores de nuestro microprocesador y el status de cada uno. Como puedes ver la administracion es completa.

5.2 Administracion (Creacion de colecciones)

Lo primero es definir una coleccion con sus atributos, en este caso crearemos una coleccion donde podremos añadir peliculas a nuestro videoclub.

Definimos la estructura de los elementos en un archivo :

films_collection_v1.json

{
    "name": "films_v1",
    "fields": [
      {
        "name": "filmId",
        "type": "string",
        "optional": false
      },
      {
        "name": "name_es_ES",
        "type": "string"
      },
      {
        "name": "name_en_GB",
        "type": "string"
      },
      {
        "name": "actors",
        "type": "string[]",
        "facet": true
      },
      {
        "name": "popularity",
        "type": "int32",
        "sort": true,
        "optional": false
      },
      {
        "name": "image",
        "type": "string",
        "facet": false
      },
      {
        "name": "quantity",
        "type": "int64",
        "optional": false
      }

    ],
    "default_sorting_field": "popularity"
  }
Cabe destacar los atributos:

  • default_sorting_field: indica cuando se haga un GET a la coleccion, las peliculas vendran en el orden marcado por ese campo (debería ser numerico obviamente)
  • type: indica el tipo de dato
  • optional: si el campo puede existir o no (en nuestro caso es obligatorio la popularidad, el id y la cantidad al menos)
  • facet: permite facetado

y creamos la coleccion v1 mediante el API

curl "http://localhost:8108/collections" \
      -X POST \
      -H "X-TYPESENSE-API-KEY: xyz" \
      --data-binary @./products_collection_v1.json

Java sdk client

Tambien podríamos haber creado la collection usando el java sdk docs

Podemos consular la collection a traves del UI web en la seccion “Collections”

Collections

5.2.1 Alta de peliculas

Vamos a dar de alta algunas peliculas y sus cantidades. El API acepta un jsonlist (jsonl), que es archivo en el que cada fila es un json completo (entre fila y fila solo hay un retorno de carro).

films.jsonl :

{"id":"0", "filmId": "001-0","name_es_ES": "Sueños de fuga", "name_en_GB": "The Shawshank Redemption", "actors": ["Tim Robbins", "Morgan Freeman", "Bob Gunton"], "popularity": 8,"image": "https://picsum.photos/200", "quantity":23  }
{"id":"1","filmId": "001-1","name_es_ES": "Origen", "name_en_GB": "Inception", "actors": ["Leonardo DiCaprio", "Joseph Gordon-Levitt", "Ellen Page"], "popularity": 1,"image": "https://picsum.photos/200", "quantity":100 }
{"id":"2","filmId": "022-2","name_es_ES": "El caballero oscuro", "name_en_GB": "The Dark Knight","actors": ["Christian Bale", "Heath Ledger", "Aaron Eckhart"],  "popularity": 2,"image": "https://picsum.photos/200", "quantity":33 } 
{"id":"3","filmId": "023-3","name_es_ES": "Pulp Fiction","name_en_GB": "Pulp Fiction", "actors": ["John Travolta", "Uma Thurman", "Samuel L. Jackson"], "popularity": 8,"image": "https://picsum.photos/200", "quantity":47 }
{"id":"4","filmId": "023-4","name_es_ES": "Forrest Gump","name_en_GB": "Forrest Gump","actors": ["Tom Hanks", "Robin Wright", "Gary Sinise"], "popularity": 5,"image": "https://picsum.photos/200", "quantity":125 }
{"id":"5","filmId": "d32-5","name_es_ES": "El padrino","name_en_GB": "The Godfather","actors": ["Marlon Brando", "Al Pacino", "James Caan"], "popularity": 5,"image": "https://picsum.photos/200", "quantity":1727 }
{"id":"6","filmId": "011-6","name_es_ES": "Matrix","name_en_GB": "Matrix","actors": ["Keanu Reeves", "Laurence Fishburne", "Carrie-Anne Moss"], "popularity": 1,"image": "https://picsum.photos/200", "quantity":345 }
{"id":"7","filmId": "011-7","name_es_ES": "Gladiator","name_en_GB": "Gladiator","actors": ["Russell Crowe", "Joaquin Phoenix", "Connie Nielsen"], "popularity": 1,"image": "https://picsum.photos/200", "quantity":876 }
{"id":"8","filmId": "077-8","name_es_ES": "El rey león","name_en_GB": "The Lion King","actors": ["Matthew Broderick", "James Earl Jones", "Jeremy Irons"], "popularity": 2,"image": "https://picsum.photos/200", "quantity":734 }
{"id":"9","filmId": "077-9","name_es_ES": "Titanic","name_en_GB": "Titanic","actors": ["Leonardo DiCaprio", "Kate Winslet", "Billy Zane"], "popularity": 5,"image": "https://picsum.photos/200", "quantity":976 }
{"id":"10","filmId":"df0-10","name_es_ES": "Los vengadores","name_en_GB": "The Avengers","actors": ["Robert Downey Jr", "Chris Hemsworth", "Scarlett Johansson"], "popularity": 0,"image": "https://picsum.photos/200", "quantity":35 }
{"id":"11","filmId":"a01-11","name_es_ES": "Parque Jurásico","name_en_GB": "Jurassic Park","actors": [ "Sam Neill", "Laura Dern", "Jeff Goldblum"], "popularity": 5,"image": "https://picsum.photos/200", "quantity":11 }
{"id":"12","filmId":"001-12","name_es_ES": "El lobo de Wall Street","name_en_GB": "The Wolf of Wall Street","actors": ["Leonardo DiCaprio", "Jonah Hill", "Margot Robbie"], "popularity": 3,"image": "https://picsum.photos/200", "quantity":437 }
{"id":"13","filmId":"023-13","name_es_ES": "El padrino: Parte II","name_en_GB": "The Godfather: Part II","actors": [ "Al Pacino", "Robert De Niro", "Diane Keaton"], "popularity": 7,"image": "https://picsum.photos/200", "quantity":221 }
{"id":"14","filmId":"002-14","name_es_ES": "El silencio de los corderos","name_en_GB": "The Silence of the Lambs","actors": [ "Jodie Foster", "Anthony Hopkins", "Lawrence A. Bonney"], "popularity": 6,"image": "https://picsum.photos/200", "quantity":732 }
{"id":"15","filmId":"000-15","name_es_ES": "La vida es bella","name_en_GB": "La vita e bella","actors": [ "Roberto Benigni", "Horst Buchholz", "Marisa Paredes"], "popularity": 12,"image": "https://picsum.photos/200", "quantity":15 }
....
....

Y publicamos a traves del API:

curl "http://localhost:8108/collections/films_v1/documents/import?action=create" \
      -X POST \
      -H "X-TYPESENSE-API-KEY: xyz" \
      --data-binary @./films.jsonl

5.2.2 Obtencion de peliculas API

Vamos a pedir un listado completo sin filtrar por nada:

curl "http://localhost:8108/collections/films_v1/documents/search?q=*" \
      -X GET \
      -H "X-TYPESENSE-API-KEY: xyz" | jq .

{
  "facet_counts": [],
  "found": 16,
  "hits": [
    {
      "document": {
        "actors": [
          "Roberto Benigni",
          "Horst Buchholz",
          "Marisa Paredes"
        ],
        "filmId": "000-15",
        "id": "15",
        "image": "https://picsum.photos/200",
        "name_en_GB": "La vita e bella",
        "name_es_ES": "La vida es bella",
        "popularity": 12,
        "quantity": 15
      },
      "highlight": {},
      "highlights": []
    },
    {
      "document": {
        "actors": [
          "John Travolta",
          "Uma Thurman",
          "Samuel L. Jackson"
        ],
        "filmId": "023-3",
        "id": "3",
        "image": "https://picsum.photos/200",
        "name_en_GB": "Pulp Fiction",
        "name_es_ES": "Pulp Fiction",
        "popularity": 8,
        "quantity": 47
      },

.....

Vemos que nos devuelve los elementos en el array hits ordenados por el “default_sorting_field”: “popularity”

5.2.3 Fast UI Creation

** En varios cdn podemos encontrar librerias de TypseSense que nos abstraen del desarrollo y podemos tener un buscador funcional en muy pocos minutos.

<script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.44.0"></script>
<script src="https://cdn.jsdelivr.net/npm/typesense-instantsearch-adapter@2/dist/typesense-instantsearch-adapter.min.js"></script>

Usaremos el ejemplo de su github oficial https://github.com/typesense/typesense-instantsearch-demo-no-npm-yarn y adaptaremos al modelo de nuestra collection:

Cambiamos la parte del conector y el widget:

<script>
    const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
        server: {
            apiKey: 'xyz', // Be sure to use an API key that only allows searches, in production
            nodes: [
                {
                    host: 'localhost',
                    port: '8108',
                    protocol: 'http',
                },
            ],
        },
        // The following parameters are directly passed to Typesense's search API endpoint.
        //  So you can pass any parameters supported by the search endpoint below.
        //  queryBy is required.
        //  filterBy is managed and overridden by InstantSearch.js. To set it, you want to use one of the filter widgets like refinementList or use the `configure` widget.
        additionalSearchParameters: {
            queryBy: 'name_es_ES,name_en_GB,actors',
        },
    });
    const searchClient = typesenseInstantsearchAdapter.searchClient;

    const search = instantsearch({
        searchClient,
        indexName: 'films_v1',
    });

    search.addWidgets([
        instantsearch.widgets.searchBox({
            container: '#searchbox',
        }),
        instantsearch.widgets.configure({
            hitsPerPage: 8,
        }),
        instantsearch.widgets.hits({
            container: '#hits',
            templates: {
                item(item) {
                    return `
                        <div>
                          <img src="${item.image}" alt="${item.name_es_ES}" height="100" />
                          <div class="hit-name">
                            ${item._highlightResult.name_es_ES.value} (${item._highlightResult.name_en_GB.value})
                          </div>
                          <div class="hit-authors">
                          ${item._highlightResult.actors.map((a) => a.value).join(', ')}
                          </div>
                          <div class="hit-publication-year">Quantity ${item.quantity}</div>
                          <div class="hit-rating">${item.popularity} rating</div>
                        </div>
                      `;
                },
            },
        }),
        instantsearch.widgets.pagination({
            container: '#pagination',
        }),
    ]);

    search.start();
</script>

Visitamos la pagina y voilá!! Los elementos paginados y ordenados por popularity

Collections

5.2.4 Search Text Box

Si nos fijamos en la instanciacion del Widget en el campo additionalSearchParameters indicamos cuales son los campos contra los que queremos matchear el valor del input de usuario, en este caso lo que metamos se contrastara contra name_es_ES, name_en_GB y actors.

additionalSearchParameters: {
            queryBy: 'name_es_ES,name_en_GB,actors',
},

5.2.5 Typotolerant

Vemos ademas como se comporta si solo buscamos por “obert”

Partial Search

En cambio vamos a buscar solo por “obe” y vemos que no obtiene ningun resultado

Not min typotolerant charts

¿es raro no? .... pues es debido a que para no estar haciendo busquedas innecesarias, se debe especificar cual es el minimo de caracteres del valor de busqueda para que se aplique la correccion y busqueda typotolerante.

Este valor es el min_len_1typo, que su valor por defecto es 4, es decir si no tiene 4 caracteres no se aplica la busqueda “typo tolerante”, tiene sentido porque imaginemos que si queremos solo buscar por el termino “a” no queremos obtener infinitos resultados..

TypoTolerant configuration

Para sacarnos las espinita, vamos a hacer la peticion indicando que sea tolerante con 3 digitos tambien y vemos como ahora si que los encuentra:

curl "http://localhost:8108/collections/films_v1/documents/search?q=obe&query_by=name_es_ES,name_en_GB,actors&min_len_1typo=3" \
      -X GET \
      -H "X-TYPESENSE-API-KEY: xyz" | jq .

{
  "facet_counts": [],
  "found": 3,
  "hits": [
    {
      "document": {
        "actors": [
          "Roberto Benigni",
          "Horst Buchholz",
          "Marisa Paredes"
        ],
        "filmId": "000-15",
        "id": "15",
        "image": "https://picsum.photos/200",
        "name_en_GB": "La vita e bella",
        "name_es_ES": "La vida es bella",
        "popularity": 12,
        "quantity": 15
      },
      "highlight": {
        "actors": [
          {
            "matched_tokens": [

.... 
.... 
.... 
      "document": {
        "actors": [
          "Al Pacino",
          "Robert De Niro",
          "Diane Keaton"
        ],
        "filmId": "023-13",
        "id": "13",
        "image": "https://picsum.photos/200",
        "name_en_GB": "The Godfather: Part II",
        "name_es_ES": "El padrino: Parte II",
        "popularity": 7,
        "quantity": 221
      },
      "highlight": {
        "actors": [
....
...
....
    {
      "document": {
        "actors": [
          "Robert Downey Jr",
          "Chris Hemsworth",
          "Scarlett Johansson"
        ],
        "filmId": "df0-10",
        "id": "10",
        "image": "https://picsum.photos/200",
        "name_en_GB": "The Avengers",
        "name_es_ES": "Los vengadores",
        "popularity": 0,
        "quantity": 35
      },
      "highlight": {
        "actors": [
          {

  ...
  ...
y continua respetando el orden de popularity.

Tienes todos los parametros de configuracion para toda esta configuracion de typotolerancia aqui:

https://typesense.org/docs/28.0/api/search.html#typo-tolerance-parameters

5.3 Reglas Analiticas

Como comentamos al principio de este post, podemos crear reglas en base a los eventos que estan ocurriendo en nuestro sistema de busqueda.

Vamos a crear una regla, de manera que cuando se consulte una determinada pelicula, su popularidad se incremente en 1 punto y si se compra una unidad de la pelicula se incremente la popularidad en 2.

Definimos la regla films_v1_click_rule.json:

{
    "name": "films_click_events",
    "type": "counter",
    "params": {
        "source": {
            "collections": ["films_v1"],
            "events":  [
                {"type": "click", "weight": 1, "name": "films_click_events"},
                {"type": "conversion","weight": 2,"name": "films_purchase_event"}

            ]
        },
        "destination": {
            "collection": "films_v1",
            "counter_field": "popularity"
        }
    }
}

TypeSense soporta estos 3 tipos de evento (click,conversion,visit) docs:

typesense-event-types.png

Usamos la operacion de API para crear las 2 reglas asociadas a la collecion films_v1:

curl "http://localhost:8108/analytics/rules" \
      -X POST \
      -H "X-TYPESENSE-API-KEY: xyz" \
      -H "Content-Type: application/json" \
      --data-binary @./films_v1_click_rule.json

5.3.1 Disparando la regla

Si recordamos, la pelicula “La vida es bella” (id:15) tiene una popularity de 12.

typesense-analitic-pre-click-event.png

Vamos a lanzar un evento indicando que alguien ha consultado esa pelicula :

curl "http://localhost:8108/analytics/events" -X POST \
     -H "X-TYPESENSE-API-KEY: xyz" \
     -d '{
            "type": "click",
            "name": "films_click_events",
            "data": {
                  "doc_id": "15",
                  "user_id": "Antonio Volkaniski Garcia"
            }
        }'

{"ok": true}

–analytics-flush-interval=60

Cuando volvemos a consultar es muy probable que no se haya incrementado el valor de popularity y esto es debido a que al cambiar un valor del registro (ahora deberia pasar a 13) se debe hacer una “mini reindexacion” que es un proceso costoso para la collecion. El campo analytics-flush-interval=60 indica que se guardaran los eventos, pero cada 60 segundos se materializaran los eventos recogidos en ese intervalo, de manera que este proceso solo se ejecuta una vez para todos los elementos modificados.

Pasados 60 segundos (analytics-flush-interval) podemos ver que se ha materilizado la regla y la pelicula ha ganado un punto en popularidad subiendo al 13.

typesense-analitic-pre-click-event.png

El evento de conversion lo lógico es que no lo disparase el front, si no un proceso de negocio en servidor, ya que va asociado a un proceso de compra. Para disparar esta regla desde JAVA , tienes aqui la documentacion y una aproximacion sería algo así:

AnalyticsEventCreateSchema analyticsEvent = new AnalyticsEventCreateSchema()
        .type("conversion")
        .name("films_purchase_event")
        .data(Map.of(
                "doc_id", "15",
                "user_id", "Paco el de los palotes",
                "amount", 1"
        ));

client.analytics().events().create(analyticsEvent);

5.4 Creacion de un alias

Es muy conveniente acceder a las consultas de las colleciones a traves de un alias.

Imaginemos un proceso de reindexacion originado porque el modelo de nuestro pelicula (films_v1) lo queremos modificar (añadimos o quitamos campos). - Es necesario reindexar otra vez todos los documentos para preparar las busquedas. - Este proceso es costoso y la collection estará bloqueada e innacesible hasta que termine el proceso. - Esto en producción es inviable.

Un alias no permite la flexibilidad, de que ese alias apunta a una coleccion en este caso lo creamos apuntando a films_v1.

curl "http://localhost:8108/aliases/films/" -X PUT \
    -H "Content-Type: application/json" \
    -H "X-TYPESENSE-API-KEY: xyz" -d '{
        "collection_name": "films_v1"
    }'

Ahora todas las consultas las harán los clientes apuntando al alias “films” que internamente consultara la collecion films_v1

curl "http://localhost:8108/collections/films/documents/search?q=*" \
      -X GET \
      -H "X-TYPESENSE-API-KEY: xyz" 
Ahora con el acceso a traves de alias, tenemos muy facil y sin perdida de servicio realizar las modificaciones:

  • Podemos crear una collection films_v2 con los nuevos campos y comenzar un proceso de migracion de datos de films_v1 a films_v2.
  • Como nuestro alias apunta a films_v1 no hay caida de servicio
  • Cuando termine la migracion, basta con modificar el alias para que apunte a films_v2 y voilá !!
curl "http://localhost:8108/aliases/films/" -X PUT \
    -H "Content-Type: application/json" \
    -H "X-TYPESENSE-API-KEY: xyz" -d '{
        "collection_name": "films_v2"
    }'

6. Conclusiones

Hemos visto que TypeSense es ligero, muy optimizado (voice_query, image_query), tipo-tolerante, enfocado al Retail a tope y sobre todo (que es lo que nos interesa a los programadores) es muy facil de usar.

Y por abrir boca… ya esta integrado en los nuevos starters de Spring Boot basados en IA aunque sea en su version 1.0.0 beta https://docs.spring.io/spring-ai/reference/api/vectordbs/typesense.html.