Los ingenieros de software ocupan un lugar apasionante en este mundo. Independientemente del sector tecnológico o de la industria, tenemos la tarea de resolver problemas que contribuyan directamente a las metas y objetivos de nuestros empleadores. Como beneficio adicional, podemos utilizar la tecnología para mitigar cualquier desafío que se nos presente.
Para este ejemplo, quería centrarme en cómo pgvector (una búsqueda de similitudes de vectores de código abierto para Postgres) se puede utilizar para identificar similitudes de datos que existen en los datos empresariales.
Como ejemplo sencillo, supongamos que el departamento de marketing necesita ayuda para una campaña que planean lanzar. El objetivo es llegar a todas las cuentas de Salesforce que se encuentran en industrias estrechamente alineadas con la industria del software.
Al final, les gustaría centrarse en cuentas de las tres industrias más similares, con la capacidad de utilizar esta herramienta en el futuro para encontrar similitudes para otras industrias. Si es posible, les gustaría tener la opción de proporcionar el número deseado de industrias coincidentes en lugar de devolver siempre las tres primeras.
Este caso de uso se centra en realizar una búsqueda de similitud. Si bien es posible completar este ejercicio manualmente, me viene a la mente la herramienta Wikipedia2Vec debido a las incorporaciones previamente entrenadas que ya se han creado para varios idiomas. Las incrustaciones de palabras, también conocidas como vectores, son representaciones numéricas de palabras que contienen información sintáctica y semántica. Al representar palabras como vectores, podemos determinar matemáticamente qué palabras son semánticamente "más cercanas" a otras.
En nuestro ejemplo, también podríamos haber escrito un programa Python simple para crear vectores de palabras para cada industria configurada en Salesforce.
La extensión pgvector
requiere una base de datos Postgres. Sin embargo, los datos empresariales de nuestro ejemplo residen actualmente en Salesforce. Afortunadamente, Heroku Connect proporciona una manera sencilla de sincronizar las cuentas de Salesforce con Heroku Postgres, almacenándolas en una tabla llamada salesforce.account
. Luego, tendremos otra tabla llamada salesforce.industries
que contiene cada industria en Salesforce (como una clave VARCHAR), junto con su vector de palabras asociado.
Con los datos de Salesforce y los vectores de palabras en Postgres, crearemos una API RESTful usando Java y Spring Boot. Este servicio realizará la consulta necesaria y devolverá los resultados en formato JSON.
Podemos ilustrar la vista de alto nivel de la solución de esta manera:
El código fuente residirá en GitLab. Emitir un comando git push heroku
activará una implementación en Heroku, introduciendo una API RESTful que el equipo de marketing puede consumir fácilmente.
Una vez implementado el diseño de alto nivel, podemos comenzar a construir una solución. Usando mi inicio de sesión de Salesforce, pude navegar a la pantalla Cuentas para ver los datos de este ejercicio. A continuación se muestra un ejemplo de la primera página de datos empresariales:
Para este esfuerzo, planeé usar Heroku para resolver la solicitud del equipo de marketing. Inicié sesión en mi cuenta Heroku y utilicé el botón Crear nueva aplicación para establecer una nueva aplicación llamada similarity-search-sfdc
:
Después de crear la aplicación, navegué a la pestaña Recursos para encontrar el complemento Heroku Postgres. Escribí "Postgres" en el campo de búsqueda de complementos.
Después de seleccionar Heroku Postgres de la lista, elegí el plan Estándar 0 , pero pgvector está disponible en ofertas de bases de datos de nivel Estándar (o superior) que ejecutan PostgreSQL 15 o la base de datos beta de nivel Esencial .
Cuando confirmé el complemento, Heroku generó y proporcionó una cadena de conexión DATABASE_URL
. Encontré esto en la sección Config Vars de la pestaña Configuración de mi aplicación. Utilicé esta información para conectarme a mi base de datos y habilitar la extensión pgvector de esta manera:
CREATE EXTENSION vector;
A continuación, busqué y encontré el complemento Heroku Connect . Sabía que esto me daría una manera fácil de conectarme a los datos empresariales en Salesforce.
Para este ejercicio, el plan gratuito Demo Edition funciona bien.
En este punto, la pestaña Recursos para la aplicación similarity-search-sfdc
tenía este aspecto:
Seguí las instrucciones de " Configuración de Heroku Connect " para vincular mi cuenta de Salesforce a Heroku Connect. Luego, seleccioné el objeto Cuenta para sincronización. Una vez completado, pude ver los mismos datos de la cuenta de Salesforce en Heroku Connect y en la base de datos subyacente de Postgres.
Desde una perspectiva SQL, lo que hice resultó en la creación de una tabla salesforce.account
con el siguiente diseño:
create table salesforce.account ( createddate timestamp, isdeleted boolean, name varchar(255), systemmodstamp timestamp, accountnumber varchar(40), industry varchar(255), sfid varchar(18), id serial primary key, _hc_lastop varchar(32), _hc_err text );
Para que la búsqueda de similitudes funcionara como se esperaba, necesitaba generar vectores de palabras para cada industria de cuentas de Salesforce:
Dado que el caso de uso principal indicaba la necesidad de encontrar similitudes para la industria del software, también necesitaríamos generar un vector de palabras para esa industria.
Para simplificar las cosas en este ejercicio, ejecuté esta tarea manualmente usando Python 3.9 y un archivo llamado embed.py
, que se ve así:
from wikipedia2vec import Wikipedia2Vec wiki2vec = Wikipedia2Vec.load('enwiki_20180420_100d.pkl') print(wiki2vec.get_word_vector('software').tolist())
Tenga en cuenta que el método get_word_vector()
espera una representación en minúsculas de la industria.
La ejecución de python embed.py
generó el siguiente vector de palabras para la palabra software
:
[-0.40402618050575256, 0.5711150765419006, -0.7885153293609619, -0.15960034728050232, -0.5692323446273804, 0.005377458408474922, -0.1315757781267166, -0.16840921342372894, 0.6626015305519104, -0.26056772470474243, 0.3681095242500305, -0.453583300113678, 0.004738557618111372, -0.4111144244670868, -0.1817493587732315, -0.9268549680709839, 0.07973367720842361, -0.17835664749145508, -0.2949991524219513, -0.5533796548843384, 0.04348105192184448, -0.028855713084340096, -0.13867013156414032, -0.6649054884910583, 0.03129105269908905, -0.24817068874835968, 0.05968991294503212, -0.24743635952472687, 0.20582349598407745, 0.6240783929824829, 0.3214546740055084, -0.14210252463817596, 0.3178422152996063, 0.7693028450012207, 0.2426985204219818, -0.6515568494796753, -0.2868216037750244, 0.3189859390258789, 0.5168254971504211, 0.11008890718221664, 0.3537853956222534, -0.713259220123291, -0.4132286608219147, -0.026366405189037323, 0.003034653142094612, -0.5275223851203918, -0.018167126923799515, 0.23878540098667145, -0.6077089905738831, 0.5368344187736511, -0.1210874393582344, 0.26415619254112244, -0.3066694438457489, 0.1471938043832779, 0.04954215884208679, 0.2045321762561798, 0.1391817331314087, 0.5286830067634583, 0.5764685273170471, 0.1882934868335724, -0.30167853832244873, -0.2122340053319931, -0.45651525259017944, -0.016777794808149338, 0.45624101161956787, -0.0438646525144577, -0.992512047290802, -0.3771328926086426, 0.04916151612997055, -0.5830298066139221, -0.01255014631897211, 0.21600870788097382, -0.18419665098190308, 0.1754663586616516, -0.1499166339635849, -0.1916201263666153, -0.22884036600589752, 0.17280352115631104, 0.25274306535720825, 0.3511175513267517, -0.20270302891731262, -0.6383468508720398, 0.43260180950164795, -0.21136239171028137, -0.05920517444610596, 0.7145522832870483, 0.7626600861549377, -0.5473887920379639, 0.4523043632507324, -0.1723199188709259, -0.10209759324789047, -0.5577948093414307, -0.10156919807195663, 0.31126976013183594, 0.3604489266872406, -0.13295558094978333, 0.2473849356174469, 0.278846800327301, -0.28618067502975464, 0.00527254119515419]
Para almacenar los vectores de palabras, necesitábamos agregar una tabla industries
a la base de datos de Postgres usando el siguiente comando SQL:
create table salesforce.industries ( name varchar not null constraint industries_pk primary key, embeddings vector(100) not null );
Con la tabla industries
creada, insertaremos cada uno de los vectores de palabras generados. Hacemos esto con declaraciones SQL similares a las siguientes:
INSERT INTO salesforce.industries (name, embeddings) VALUES ('Software','[-0.40402618050575256, 0.5711150765419006, -0.7885153293609619, -0.15960034728050232, -0.5692323446273804, 0.005377458408474922, -0.1315757781267166, -0.16840921342372894, 0.6626015305519104, -0.26056772470474243, 0.3681095242500305, -0.453583300113678, 0.004738557618111372, -0.4111144244670868, -0.1817493587732315, -0.9268549680709839, 0.07973367720842361, -0.17835664749145508, -0.2949991524219513, -0.5533796548843384, 0.04348105192184448, -0.028855713084340096, -0.13867013156414032, -0.6649054884910583, 0.03129105269908905, -0.24817068874835968, 0.05968991294503212, -0.24743635952472687, 0.20582349598407745, 0.6240783929824829, 0.3214546740055084, -0.14210252463817596, 0.3178422152996063, 0.7693028450012207, 0.2426985204219818, -0.6515568494796753, -0.2868216037750244, 0.3189859390258789, 0.5168254971504211, 0.11008890718221664, 0.3537853956222534, -0.713259220123291, -0.4132286608219147, -0.026366405189037323, 0.003034653142094612, -0.5275223851203918, -0.018167126923799515, 0.23878540098667145, -0.6077089905738831, 0.5368344187736511, -0.1210874393582344, 0.26415619254112244, -0.3066694438457489, 0.1471938043832779, 0.04954215884208679, 0.2045321762561798, 0.1391817331314087, 0.5286830067634583, 0.5764685273170471, 0.1882934868335724, -0.30167853832244873, -0.2122340053319931, -0.45651525259017944, -0.016777794808149338, 0.45624101161956787, -0.0438646525144577, -0.992512047290802, -0.3771328926086426, 0.04916151612997055, -0.5830298066139221, -0.01255014631897211, 0.21600870788097382, -0.18419665098190308, 0.1754663586616516, -0.1499166339635849, -0.1916201263666153, -0.22884036600589752, 0.17280352115631104, 0.25274306535720825, 0.3511175513267517, -0.20270302891731262, -0.6383468508720398, 0.43260180950164795, -0.21136239171028137, -0.05920517444610596, 0.7145522832870483, 0.7626600861549377, -0.5473887920379639, 0.4523043632507324, -0.1723199188709259, -0.10209759324789047, -0.5577948093414307, -0.10156919807195663, 0.31126976013183594, 0.3604489266872406, -0.13295558094978333, 0.2473849356174469, 0.278846800327301, -0.28618067502975464, 0.00527254119515419] ');
Tenga en cuenta que, si bien creamos un vector de palabras con la representación en minúsculas de la industria del software (software), la columna industries.name
debe coincidir con el nombre de la industria en mayúscula (Software).
Una vez que todos los vectores de palabras generados se hayan agregado a la tabla industries
, podemos cambiar nuestro enfoque para introducir una API RESTful.
Este fue el punto en el que mi pasión como ingeniero de software se aceleró porque tenía todo en su lugar para resolver el desafío en cuestión.
A continuación, utilizando Spring Boot 3.2.2 y Java (temurin) 17, creé el proyecto similarity-search-sfdc
en IntelliJ IDEA con las siguientes dependencias de Maven:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.pgvector</groupId> <artifactId>pgvector</artifactId> <version>0.1.4</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
Creé entidades simplificadas tanto para el objeto Cuenta como para el objeto Industria (incrustación), que se alinearon con las tablas de la base de datos de Postgres creadas anteriormente.
@AllArgsConstructor @NoArgsConstructor @Data @Entity @Table(name = "account", schema = "salesforce") public class Account { @Id @Column(name = "sfid") private String id; private String name; private String industry; } @AllArgsConstructor @NoArgsConstructor @Data @Entity @Table(name = "industries", schema = "salesforce") public class Industry { @Id private String name; }
Usando la interfaz JpaRepository, agregué las siguientes extensiones para permitir un fácil acceso a las tablas de Postgres:
public interface AccountsRepository extends JpaRepository<Account, String> { @Query(nativeQuery = true, value = "SELECT sfid, name, industry " + "FROM salesforce.account " + "WHERE industry IN (SELECT name " + " FROM salesforce.industries " + " WHERE name != :industry " + " ORDER BY embeddings <-> (SELECT embeddings FROM salesforce.industries WHERE name = :industry) " + " LIMIT :limit)" + "ORDER BY name") Set<Account> findSimilaritiesForIndustry(String industry, int limit); } public interface IndustriesRepository extends JpaRepository<Industry, String> { }
Observe que el método findSimilaritiesForIndustry()
es donde se realizará todo el trabajo pesado para resolver este caso de uso. El método aceptará los siguientes parámetros:
industry
: la industria para encontrar similitudes.limit
: el número máximo de similitudes de la industria para buscar al consultar cuentas
Tenga en cuenta el operador de distancia euclidiana (<->) en nuestra consulta anterior. Este es el operador integrado de la extensión para realizar la búsqueda de similitud.
Con el caso de uso original de la industria de "Software" y un límite establecido para las tres industrias más cercanas, la consulta que se ejecuta se vería así:
SELECT sfid, name, industry FROM salesforce.account WHERE industry IN (SELECT name FROM salesforce.industries WHERE name != 'Software' ORDER BY embeddings <-> (SELECT embeddings FROM salesforce.industries WHERE name = 'Software') LIMIT 3) ORDER BY name;
A partir de ahí, construí la clase AccountsService
para interactuar con los repositorios JPA:
@RequiredArgsConstructor @Service public class AccountsService { private final AccountsRepository accountsRepository; private final IndustriesRepository industriesRepository; public Set<Account> getAccountsBySimilarIndustry(String industry, int limit) throws Exception { List<Industry> industries = industriesRepository.findAll(); if (industries .stream() .map(Industry::getName) .anyMatch(industry::equals)) { return accountsRepository .findSimilaritiesForIndustry(industry, limit); } else { throw new Exception( "Could not locate '" + industry + "' industry"); } } }
Por último, hice que la clase AccountsController
proporcionara un punto de entrada RESTful y me conectara a AccountsService
:
@RequiredArgsConstructor @RestController @RequestMapping(value = "/accounts") public class AccountsController { private final AccountsService accountsService; @GetMapping(value = "/similarities") public ResponseEntity<Set<Account>> getAccountsBySimilarIndustry(@RequestParam String industry, @RequestParam int limit) { try { return new ResponseEntity<>( accountsService .getAccountsBySimilarIndustry(industry, limit), HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } } }
Con el servicio Spring Boot listo, agregué el siguiente Procfile
al proyecto, permitiéndole a Heroku saber más sobre nuestro servicio:
web: java $JAVA_OPTS -Dserver.port=$PORT -jar target/*.jar
Para estar seguro, agregué el archivo system.properties
para especificar qué versiones de Java y Maven se esperan:
java.runtime.version=17 maven.version=3.9.5
Usando la CLI de Heroku, agregué un control remoto a mi repositorio de GitLab para el servicio similarity-search-sfdc
a la plataforma Heroku:
heroku git:remote -a similarity-search-sfdc
También configuro el tipo de paquete de compilación para el servicio similarity-search-sfdc
mediante el siguiente comando:
heroku buildpacks:set https://github.com/heroku/heroku-buildpack-java
Finalmente, implementé el servicio similarity-search-sfdc
en Heroku usando el siguiente comando:
git push heroku
Ahora, la pestaña Recursos para la aplicación similarity-search-sfdc
apareció como se muestra a continuación:
Con la API RESTful en ejecución, emití el siguiente comando cURL para ubicar las tres principales industrias de Salesforce (y cuentas asociadas) más cercanas a la industria del software :
curl --location 'https://HEROKU-APP-ROOT-URL/accounts/similarities?industry=Software&limit=3'
La API RESTful devuelve un estado de respuesta 200 OK HTTP
junto con la siguiente carga útil:
[ { "id": "001Kd00001bsP80IAE", "name": "CleanSlate Technology Group", "industry": "Technology" }, { "id": "001Kd00001bsPBFIA2", "name": "CMG Worldwide", "industry": "Media" }, { "id": "001Kd00001bsP8AIAU", "name": "Dev Spotlight", "industry": "Technology" }, { "id": "001Kd00001bsP8hIAE", "name": "Egghead", "industry": "Electronics" }, { "id": "001Kd00001bsP85IAE", "name": "Marqeta", "industry": "Technology" } ]
Como resultado, las industrias de tecnología , medios y electrónica son las industrias más cercanas a la industria del software en este ejemplo.
Ahora, el departamento de marketing tiene una lista de cuentas con las que pueden contactar para su próxima campaña.
Hace años, pasé más tiempo del que me gustaría admitir jugando al videojuego multijugador Team Fortress 2 . Aquí hay una captura de pantalla de un evento de 2012 que fue muy divertido:
Aquellos que estén familiarizados con este aspecto de mi vida podrán decirles que mi elección predeterminada de clase de jugador era el soldado. Esto se debe a que el soldado tiene el mejor equilibrio entre salud, movimiento, velocidad y potencia de fuego.
Siento que los ingenieros de software son la "clase de soldados" del mundo real porque podemos adaptarnos a cualquier situación y centrarnos en brindar soluciones que cumplan con las expectativas de manera eficiente.
Desde hace algunos años, me he centrado en la siguiente declaración de misión, que creo que puede aplicarse a cualquier profesional de TI:
“Concentre su tiempo en ofrecer características/funcionalidades que amplíen el valor de su propiedad intelectual. Aproveche los marcos, productos y servicios para todo lo demás”.
- J. Vester
En el ejemplo de esta publicación, pudimos aprovechar Heroku Connect para sincronizar datos empresariales con una base de datos de Postgres. Después de instalar la extensión pgvector, creamos vectores de palabras para cada industria única a partir de esas cuentas de Salesforce. Finalmente, introdujimos un servicio Spring Boot, que simplificó el proceso de localizar cuentas de Salesforce cuya industria estaba más cercana a otra industria.
Resolvimos este caso de uso rápidamente con tecnologías de código abierto existentes, la adición de un pequeño servicio Spring Boot y Heroku PaaS, cumpliendo plenamente con mi declaración de misión. No puedo imaginar cuánto tiempo se necesitaría sin estos marcos, productos y servicios.
Si está interesado, puede encontrar el código fuente original de este artículo en GitLab:
https://gitlab.com/johnjvester/similarity-search-sfdc
¡Que tengas un gran día!