FediSearch
Tags: groovy, openai, fediverso, fediverse
Published: 2023-07-05 13:30:30

---
title: FediSearch
published: true
date: 2023-06-30 00:00:00 UTC
tags: groovy,openai,fediverso,fediverse
canonical_url: https://jorge.aguilera.soy/blog/2023/fedisearch.html
---

<dl>
<dt>INFO</dt>
<dd>
<p>En este post voy a contar mis primeros pasos con OpenAI asi que como todo primer paso
estaré equivocado y seguramente cometeré alguna burrada …​ pero a mí me ha funcionado</p>
</dd>
</dl>

Aunque ya hay gente dedicada de lleno al tema de OpenAI y relacionados, la verdad es que yo todavía no he tocado nada de esta tecnología y sólo conozco lo poquito de algún que otro artículo que haya podido leer asi como los típicos post en Linkedin de gente haciendo prompts o como se diga. Supongo que algún día me tendré que arremangar y ponerme más con ello.

Sin embargo, este mes en el trabajo me han "encasquetado" integrar un producto que no había oído nunca y dar soporte al programador que lo necesitaba. El producto en cuestión es Milvus y la movida era potenciar el buscador de nuestra aplicación usando OpenAI en lugar de los típicos "where like %" en la base de datos.

<dl>
<dt>WARNING</dt>
<dd>
<p>Ahora ya sé que podíamos haber usado Postgresql con menos esfuerzo</p>
</dd>
</dl>

Básicamente la idea de lo que queríamos hacer era:

- tenemos una base de datos de artículos con varios campos de interés para buscar, como el típico de descripción, sector, familia, etc

- vamos a "vectorizar" la unión de todos ellos. Esto significa que le vamos a pasar todos estos campos a OpenAI (un API disponible en Internet y que es propietario de una gente muy lista) para que le aplique un modelo suyo y nos devuelva un array de números (el vector). Este vector "representa" ese texto en una matriz de muchísimas dimensiones

- lo insertamos en Milvus y le decimos qué campo es el vector

- dada una búsqueda del usuario con "texto libre", la vectorizamos y le decimos a Milvus que nos busque similitudes con un grado de afinidad

Y mientras estaba liado con la parte de instalar e ir entendiendo Milvus, un buen amigo me pasó por otro lado un artículo que por casualidad me ayudó a entender mejor la historia y que además me descubría una nueva plataforma , Supabase, donde poder desplegar aplicaciones con una capa gratuita interesante.

<dl>
<dt>INFO</dt>
<dd>
<p>El artículo en cuestión: <a href="https://supabase.com/blog/openai-embeddings-postgres-vector">https://supabase.com/blog/openai-embeddings-postgres-vector</a></p>
</dd>
</dl>

## Supabase

La idea de Supabase me ha enganchado y tengo que estrujarla más pero así, en dos palabras, me dan un Postgresql donde puedo instalar con un simple click muchas de sus extensiones (Postgresql tiene más de 1.000 extensiones) y desplegar functions que acceden a los datos creando así un API.

Digamos que lo veo (tengo que explorarlo más) como el backend para mi frontend en Netlify

Una vez registrado en la plataforma simplemente he creado un proyecto nuevo, he activado el plugin pg-vector y creado una tabla "users"

Siguiendo los pasos del articulo anterior y cambiando su documents por mi users he tenido que crear la funcion de Postgresql y el índice, pero es basicamente un c&p del artículo

## OpenAI

Por lo que he entendido hay miles de proyectos que hacen lo mismo que OpenAI pero como este es el más famoso y su API es realmente sencilla es el que voy a usar. Simplemente me registro en la plataforma y obtengo un API key para poder hacer N llamadas por minuto (suficientes para este proyecto)

## FediSearch

Obviamente, parafraseando a Jose Luis Lopez Vazquez "soy como un americano que si le das una pistola tiene que usarla", así que me he hecho este pequeño proyecto para probarlo.

<dl>
<dt>INFO</dt>
<dd>
<p>Aquí tienes el video por si te quieres saltar la parte técnica y verlo en acción <a href="https://fediverse.tv/w/iiH8mw2J9D3KV2eyxBJ4Rz">https://fediverse.tv/w/iiH8mw2J9D3KV2eyxBJ4Rz</a></p>
</dd>
</dl>

La idea es crear un buscador de perfiles en Mastodon (en realidad Fediverso pero la gente lo conoce más por esta aplicación que lo usa).

A diferencia de Twitter, donde sólo hay un "único servidor", en el Fediverso hay muchísimos (se conocen como instancias, nodos, servidores …​) y todos exponen el directorio de usuarios registrados en una url. Para cada usuario puedes ver el username, su bio y campos descriptivos que el usuario haya querido añadir

Si por ejemplo consultas esta url [https://mastodon.social/api/v1/directory](https://mastodon.social/api/v1/directory) puedes ver que te responde con

```
[
    {
        "id":"109244873849193955",
        "username":"jikodesu",
        "acct":"jikodesu",
        "display_name":"Jiko",
        ...
        "created_at":"2022-10-28T00:00:00.000Z",
        "note":"\u003cp\u003eTrying out Mastodon. Part of 2022 Twitter migration\u003c/p\u003e\u003cp\u003eDuolingo user: Nihongo, ...
    },
    {
        "id":"......"
    }
```

Cada vez que consultas esta url te devuelve usuarios random.

De esta forma ya tenemos la primera parte del proyecto: obtener de cada usuario un campo de texto que lo describa.

La segunda parte del proyecto será enviarle este campo descriptivo a OpenAI para que lo vectorize

La tercera parte del proyecto será insertar en nuestra base de datos de usuarios el username, el campo de texto y el vector

Para ello he hecho un pequeño script (en Groovy of course, pero con un poco de maña lo podría haber hecho incluso con un bash) que dada una instancia del fediverso extrae la info y se la manda a OpenAI para luego insertar los resultados en Supabase:

```
@Grab(group='org.apache.commons', module='commons-lang3', version='3.12.0')

import groovy.json.*
import java.net.http.*
import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4

json = new JsonSlurper().parse("${args[0]}/api/v1/directory".toURL())

// extraemos los campos de interes de cada usuario
users = json.collect { user->
	[username: user.username,
	 note: user.display_name +"|"+escapeHtml4(user.note)+"|"+user.fields.collect{it.name+'|'+it.value}.join('|'),
	 url: user.url]
}

body = [
	input: users.collect{it.note},
	model: "text-embedding-ada-002"
]
// se los mandamos a openai para que los vectorize
request = HttpRequest.newBuilder(new URI("https://api.openai.com/v1/embeddings"))
                .headers(
                        "Content-Type", "application/json",
                        "Authorization", "Bearer ${args[1]}"
                )
                .POST(HttpRequest.BodyPublishers.ofString(JsonOutput.toJson(body).toString()))
                .build()
response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofInputStream())
result = response.body().text

// añadimos a cada usuario su vector
embeddings= new JsonSlurper().parseText(result) as Map
embeddings.data.each{ def entry ->
	users[entry.index].embedding = entry.embedding
}

// se los mandamos a Supabase con un POST
request = HttpRequest.newBuilder(new URI("https://TUPROJECTO.supabase.co/rest/v1/users"))
		.headers(
				"Content-Type", "application/json",
				"apikey", args[2]
		)
		.POST(HttpRequest.BodyPublishers.ofString(JsonOutput.toJson(users).toString()))
		.build()
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofInputStream())
```

Es un script muy basico y guarro que pide por parametros la instancia, la key de OpenAI y la key de Supabase

## Buscando usuarios

Para la búsqueda me he peleado un poco con node, deno y el subase-cli para poder crear una función que se pueda invocar para realizar la busqueda (lo que sería mi API).

Una vez he conseguido crear el proyecto y entender cómo desplegar las functions he podido desplegar mi funcion (copiada prácticamente del articulo de supabase):

```
import { serve } from 'https://deno.land/std@0.170.0/http/server.ts'
import 'https://deno.land/x/xhr@0.2.1/mod.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.5.0'
import { Configuration, OpenAIApi } from 'https://esm.sh/openai@3.1.0'

export const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

serve(async (req) => {
  // Handle CORS
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  // Search query is passed in request payload
  const { query } = await req.json()

  // OpenAI recommends replacing newlines with spaces for best results
  const input = query.replace(/\n/g, ' ')
  console.log(`Searching users for ${input}`)
  const configuration = new Configuration({ apiKey: 'sk-----------' })
  const openai = new OpenAIApi(configuration)

  // Generate a one-time embedding for the query itself
  const embeddingResponse = await openai.createEmbedding({
    model: 'text-embedding-ada-002',
    input,
  })

  const [{ embedding }] = embeddingResponse.data.data

  const supabaseClient = createClient('https://pqlvxllouutlnnnwnlxp.supabase.co', 'TU_API_TOKEN');

  // In production we should handle possible errors
  const { data: documents } = await supabaseClient.rpc('match_users', {
    query_embedding: embedding,
    match_threshold: 0.75, // Choose an appropriate threshold for your data
    match_count: 50, // Choose the number of matches
  })

  return new Response(JSON.stringify(documents.sort((a,b)=>a.similarity - b.similarity)), {
    headers: { ...corsHeaders, 'Content-Type': 'application/json' },
  })
})
```

En sí no tiene mucho interés la funcion salvo:

- Llama a OpenAPI usando mi token para vectorizar la query enviada por el usuario

- Usando la librería de supabase ejecuto un RPC llamando a la funcion embebida en el Postgres que realiza la busqueda de simimilitud por vectores

## Conclusiones

A parte de jugar un poco con el api de OpenAI y entender un poco lo de vectorizar textos creo que Supabase tiene mucho potencial pues poder disponer de un Postgresql con un solo click y añadirle funciones es muy interesante para esos pet-projects que se me ocurren cada cierto tiempo