¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
dev
Daniel Peña 14/04/2025 Cargando comentarios…
Typesense es un software search engine de código abierto que permite su instalación on-premise, aunque también ofrece su servicio SaaS. Se caracteriza porque es un motor de búsqueda ligero, de código abierto, soporta typo-tolerance, voice-query, image-query y un largo etcétera. Está muy enfocado al retail, donde este tipo de productos son el pilar.
Nos ofrece una búsqueda vectorial donde se optimizan las búsquedas por similitud, siendo muy potente con errores tipográficos ya que se basa en algoritmos de LLM para determinar cómo crear los vectores afines.
Nos ofrece una búsqueda vectorial donde se optimizan las búsquedas por similitud, siendo muy potente con errores tipográficos ya que se basa en algoritmos de LLM para determinar cómo crear los vectores afines.
Si bien una búsqueda “%{like}%” nos permite detectar un patrón, ese valor del {like}, aunque sea pequeño, debe existir como tal y pertenecer a un valor de la tupla.
En Typesense (más enfocado a DB Document), la búsqueda se hará por uno o varios campos de la entidad a buscar y, además, será tolerante a errores tipográficos sin que tengamos que especificar nada en la búsqueda.
Otro punto a favor es que incluye reglas para eventos ocurridos en las colecciones de elementos. En un buscador de retail son muy importantes los conceptos de popularidad y orden. Nuestro sistema de búsqueda, debe “estar vivo”, es decir, nutrirse de los eventos que están ocurriendo durante su funcionamiento.
De esta manera, nuestro buscador deberá mostrar los productos más 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 debería salir de los primeros en los listados, así como si el producto realiza una conversión (se vende), debería incrementar su popularidad más que si solo es buscado.
Para todo este tipo de cosas, Typesense nos facilita el trabajo.
Además de las características que hemos comentado antes, Typesense nos ofrece:
Permite, por lo tanto, una clusterización y sincronización de nodos que nos dará ese escalado horizontal que necesitaremos en entornos productivos.
Typesense tiene muchísimas librerías de cliente que puedes utilizar para un montón de lenguajes de programación.
Además, también dispone de librerías de integración web que, con pocas líneas de código, podremos montar un buscador totalmente funcional de una forma extremadamente sencilla.
En la parte de servidores, ofrece clientes que hacen de wrapper para la integración sencilla al API HTTP de los servidores de Typesense.
Aplicamos la teoría y vamos a implementar un buscador para una tienda paso a paso.
Desplegamos una imagen docker para jugar con el servidor. Para ello 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
Aunque sea autoexplicativo, cabe mencionar que el parámetro –enable-search-analytics=true es necesario establecerlo a “true” para poder usar las reglas analíticas que comentamos anteriormente.
Por defecto, los servidores de Typsense no traen una administración web, pero puedes usar esta utilidad para tener una administración en local. Al entrar en la web, indica tus datos para localhost, que basta con indicar el apikey: “xyz”.
Una vez hemos entrado, accedemos al dashboard donde vemos todos los cores de nuestro microprocesador y el status de cada uno. Como puedes ver, la administración es completa.
Lo primero que debemos hacer es definir una colección con sus atributos. En este caso, crearemos una colección donde podremos añadir películas 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:
A continuación, creamos la colección v1 mediante el API:
curl "http://localhost:8108/collections" \
-X POST \
-H "X-TYPESENSE-API-KEY: xyz" \
--data-binary @./products_collection_v1.json
NOTA: también podríamos haber creado la collection usando el Java SDK.
Podemos consultar la colección a través del UI web en la sección “Collections”:
Vamos a dar de alta algunas películas y sus cantidades. El API acepta un JSONList (.jsonl), que es un 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 }
....
....
A continuación lo publicamos a través 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
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
},
.....
Nos devuelve los elementos en el array hits ordenados por el “default_sorting_field”: “popularity”.
Podemos encontrar librerías de Typsesense en varios CDN que nos abstraen del desarrollo y conseguir así 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 y lo adaptaremos al modelo de nuestra colección. 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 página y... voilà! Tenemos los elementos paginados y ordenados por popularidad.
Si nos fijamos en la instanciación del widget en el campo additionalSearchParameters, indicamos cuáles son los campos contra los que queremos matchear el valor del input de usuario. En este caso, lo que metamos se contrastará contra name_es_ES, name_en_GB y actors.
additionalSearchParameters: {
queryBy: 'name_es_ES,name_en_GB,actors',
},
Vemos cómo se comporta si solo buscamos por “obert”:
En cambio, si buscamos solo por “obe”, vemos que no obtenemos ningún resultado:
¿Es raro, no? Esto se debe a que, para no estar haciendo búsquedas innecesarias, es necesario especificar cuál es el mínimo de caracteres del valor de búsqueda para que se aplique la corrección y búsqueda tipotolerante.
Este valor es el min_len_1typo, cuyo valor por defecto es 4. Si una búsqueda no tiene 4 caracteres como mínimo, no se aplica la búsqueda “typo tolerante”. Esto tiene lógica, ya que si buscásemos por el término “a”, obtendríamos infinitos resultados, y no es nuestro objetivo.
Para sacarnos esa espinita, vamos a hacer la petición indicando que sea tolerante con un mínimo de 3 dígitos y vemos cómo ahora sí que muestra resultados:
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": [
{
...
...
Además, continúa respetando el orden de popularidad. Aquí puedes consultar todos los parámetros de configuración de tipotolerancia.
Como comentamos al principio de este post, podemos crear reglas en base a los eventos que están ocurriendo en nuestro sistema de búsqueda.
Vamos a crear una regla de manera que, cuando se consulte una determinada película, su popularidad se incremente en 1 punto y, si se compra una unidad de la película, 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 y visit:
Usamos la operación de API para crear las 2 reglas asociadas a la colección 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
Si recordamos, la película de “La vida es bella” (id:15) tiene una popularidad de 12. Vamos a lanzar un evento indicando que alguien ha consultado esa película:
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}
Nota: cuando volvemos a consultar, es muy probable que no se haya incrementado el valor de popularidad ya que, al cambiar un valor del registro (ahora debería pasar a 13), se debe hacer una “mini reindexación” que es un proceso costoso para la colección. El campo analytics-flush-interval=60 indica que se guardarán los eventos, pero cada 60 segundos se materializarán 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 materializado la regla y la película ha ganado un punto en popularidad, subiendo al 13.
En el caso del evento de conversión, sería lógico que no se disparase desde el frontal sino desde un proceso de negocio en el servidor, ya que va asociado a un proceso de compra. En la documentación oficial podemos ver cómo disparar esta regla desde Java, y esta sería una aproximación:
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);
Es muy conveniente acceder a las consultas de las colecciones a través de un alias.
Imaginemos un proceso de reindexación originado porque queremos modificar el modelo de nuestra película (films_v1).
Como un alias no tiene la flexibilidad de apuntar a una colección, 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 se harán apuntando al alias “films”, que internamente consultará la colección films_v1.
curl "http://localhost:8108/collections/films/documents/search?q=*" \
-X GET \
-H "X-TYPESENSE-API-KEY: xyz"
Con el acceso a través de alias, es muy fácil realizar las modificaciones sin pérdida de servicio:
curl "http://localhost:8108/aliases/films/" -X PUT \
-H "Content-Type: application/json" \
-H "X-TYPESENSE-API-KEY: xyz" -d '{
"collection_name": "films_v2"
}'
Hemos visto que Typesense es ligero, muy optimizado (voice_query, image_query), tipo-tolerante, enfocado al retail a tope y, sobre todo, es muy fácil de usar.
Además, ya está integrado en los nuevos starters de Spring Boot basados en IA, aunque sea en su versión 1.0.0 Beta.
Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.
Cuéntanos qué te parece.