Construye un flujo de trabajo de búsqueda de IA financiera utilizando LangGraph.js y Elasticsearch
Aprende a usar LangGraph.js con Elasticsearch para crear un flujo de trabajo de búsqueda financiera impulsado por IA que convierte consultas en lenguaje natural en filtros dinámicos y condicionales para análisis de inversión y mercado.
Construir aplicaciones de búsqueda con IA suele implicar la coordinación de múltiples tareas, la recuperación de datos y la extracción de datos en un flujo de trabajo sin problemas. LangGraph simplifica este proceso permitiendo a los desarrolladores orquestar agentes de IA mediante una estructura basada en nodes. En este artículo, explicaremos cómo construir una solución financiera usando LangGraph.js
¿Qué es LangGraph?
LangGraph es un marco de trabajo para construir agentes de IA y orquestarlos en un flujo de trabajo para crear aplicaciones asistidas por IA. LangGraph tiene una arquitectura de nodes donde podemos declarar funciones que representan tareas y asignarlas como nodes del flujo de trabajo. El resultado de la interacción de varios nodes será un grafo. LangGraph es parte del ecosistema más amplio LangChain, que proporciona herramientas para construir sistemas de IA modulares y componibles.
Para explicar mejor por qué LangGraph es útil, vamos a usarlo para resolver una situación problemática.
Visión general de la solución
En una firma de capital de riesgo, los inversores tienen acceso a una gran base de datos con muchas opciones de filtrado, pero cuando uno quiere combinar criterios, se vuelve difícil y lento. Esto puede hacer que algunas iniciativas relevantes no se encuentren para la inversión. Además, implica pasar muchas horas intentando identificar a los mejores candidatos, o incluso perder oportunidades.
Con LangGraph y Elasticsearch, podemos realizar búsquedas filtradas utilizando lenguaje natural, eliminando la necesidad de que los usuarios construyan manualmente solicitudes complejas con docenas de filtros. Para hacerlo más flexible, el flujo de trabajo decide automáticamente (basándose en la entrada del usuario) entre dos tipos de consulta:
Consultas centradas en la inversión: estas se dirigen a aspectos financieros y de financiación de las startups, como rondas de financiación, valoración o ingresos. Ejemplo: “Encuentra startups con financiamiento Serie A o Serie B entre $8M y $25M e ingresos mensuales superiores a $500K”.
Consultas centradas en el mercado: estas se concentran en verticales de la industria, mercados geográficos o modelos de negocio, ayudando a identificar oportunidades en sectores o regiones específicos. Ejemplo: “Encuentra startups de fintech y salud en San Francisco, Nueva York o Boston”.
Para mantener la solidez de las consultas, haremos que el LLM cree plantillas de búsqueda en lugar de consultas DSL completas. De esta manera, siempre obtienes la consulta que deseas, y el LLM solo tiene que completar los espacios en blanco y no cargar con la responsabilidad de construir la consulta que necesitas cada vez.
Lo que necesitas para comenzar
Clave de API de Elasticsearch
Clave de API de OpenAPI
Node 18 o más reciente
Instrucciones paso a paso
En esta sección, mostramos cómo se verá la app. Para ello, emplearemos TypeScript, un superconjunto de JavaScript que agrega tipos estáticos para hacer el código más fiable, fácil de mantener y seguro, detectando errores pronto mientras se mantiene totalmente compatible con el JavaScript existente.
El flujo de los nodos será el siguiente:

La imagen de arriba es generada por LangGraph y representa el flujo de trabajo que define el orden de ejecución y la lógica condicional entre nodos:
decideStrategy: utiliza un LLM para analizar la consulta del usuario y decidir entre dos estrategias de búsqueda especializadas, como centrada en la inversión u orientada al mercado.
prepareInvestmentSearch: extrae valores de filtro de la consulta y construye una plantilla predefinida que destaca los parámetros financieros y de financiación.
prepareMarketSearch: extrae también los valores del filtro, pero construye dinámicamente parámetros que enfatizan el mercado, la industria y el contexto geográfico.
executeSearch: envía la consulta construida a Elasticsearch usando una plantilla de búsqueda y recupera los documentos de startups correspondientes.
visualizarResultados: formatea los resultados finales en un resumen claro y legible que muestre atributos clave de la startup, como financiación, industria e ingresos.
Este flujo incluye una ramificación condicional, que funciona como una declaración “si” que determina si se debe usar la ruta de búsqueda de inversión o de mercado según la entrada del usuario. Esta lógica de decisión, impulsada por el LLM, hace que el flujo de trabajo sea adaptativo y consciente del contexto, un mecanismo que exploraremos con más detalle en las siguientes secciones.
Estado de LangGraph
Antes de ver cada node individualmente, necesitamos entender cómo se comunican y comparten datos. Para ello, LangGraph nos permite definir el estado del flujo de trabajo. Esto define el estado compartido que se pasará entre los nodes.
El estado actúa como un contenedor compartido que almacena datos intermedios a lo largo del flujo de trabajo: comienza con la consulta en lenguaje natural del usuario, luego guarda la estrategia de búsqueda seleccionada, los parámetros preparados para Elasticsearch, los resultados de búsqueda recuperados y, finalmente, la salida formateada.
Esta estructura permite que cada node lea y actualice el estado, asegurando un flujo coherente de información desde la entrada del usuario hasta la visualización final.
const VCState = Annotation.Root({
input: Annotation<string>(), // User's natural language query
searchStrategy: Annotation<string>(), // Search strategy chosen by LLM
searchParams: Annotation<any>(), // Prepared search parameters
results: Annotation<any[]>(), // Search results
final: Annotation<string>(), // Final formatted response
});Configure la aplicación
Todo el código de esta sección se puede encontrar en el repositorio elasticsearch-labs.
Abra un terminal en la carpeta donde estará la app e inicialice una aplicación Node.js con el comando:
npm init -yAhora podemos instalar las dependencias necesarias para este proyecto:
npm install @elastic/elasticsearch @langchain/langgraph @langchain/openai @langchain/core dotenv zod && npm install --save-dev @types/node tsx typescript@elastic/elasticsearch: nos ayuda a gestionar las solicitudes de Elasticsearch, como la ingesta y la recuperación de datos.@langchain/langgraph: Dependencia de JS para proporcionar todas las herramientas de LangGraph.@langchain/openai: cliente de OpenAI LLM para LangChain.@langchain/core: proporciona los bloques fundamentales del núcleo para las apps de LangChain, incluidas las plantillas de prompts.
dotenv: dependencia necesaria para usar variables de entorno en JavaScript.zod: dependencia para escribir datos.
@types/node tsx typescript nos permite escribir y ejecutar código TypeScript.
Ahora crea los siguientes archivos:
elasticsearchSetup.ts: creará los mapping de índice, cargará el conjunto de datos desde un archivo JSON e ingerirá los datos en Elasticsearch.main.ts: incluirá la aplicación LangGraph..env: archivo para almacenar las variables de entorno
En el archivo .env, agreguemos las siguientes variables de entorno:
ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"La clave API de OpenAPI no se usará directamente en el código; en su lugar, se usará internamente por la biblioteca @langchain/openai.
Toda la lógica relacionada con la creación de mapping, la creación de plantillas de búsqueda y la ingesta de sets de datos se encuentra en el archivo elasticsearchSetup.ts. En los próximos pasos, nos centraremos en el archivo main.ts . Además, puedes consultar los sets de datos para entender mejor cómo se ven los datos en el dataset.json.
Aplicación LangGraph
En el archivo main.ts, vamos a importar algunas dependencias necesarias para consolidar la aplicación LangGraph. En este archivo, también debes incluir las funciones del node y la declaración de estado. La declaración del grafo se realizará en un método main en los siguientes pasos. El archivo elasticsearchSetup.ts contendrá ayudantes de Elasticsearch que vamos a usar dentro de los nodes en los próximos pasos.
import { writeFileSync } from "node:fs";
import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import {
esClient,
ingestDocuments,
createSearchTemplates,
INDEX_NAME,
INVESTMENT_FOCUSED_TEMPLATE,
MARKET_FOCUSED_TEMPLATE,
createIndex,
} from "./elasticsearchSetup.js";
const llm = new ChatOpenAI({ model: "gpt-4o-mini" });Como se mencionó anteriormente, el cliente LLM se utilizará para generar los parámetros de la plantilla de búsqueda de Elasticsearch basados en la pregunta del usuario.
async function saveGraphImage(app: any): Promise<void> {
try {
const drawableGraph = app.getGraph();
const image = await drawableGraph.drawMermaidPng();
const arrayBuffer = await image.arrayBuffer();
const filePath = "./workflow_graph.png";
writeFileSync(filePath, new Uint8Array(arrayBuffer));
console.log(`📊 Workflow graph saved as: ${filePath}`);
} catch (error: any) {
console.log("⚠️ Could not save graph image:", error.message);
}
}El método anterior genera la imagen del grafo en formato png y utiliza la API de Mermaid.INK en segundo plano. Esto es útil si deseas ver cómo interactúan los nodes de la app con una visualización estilizada.
Nodes LangGraph
Ahora veamos cada node en detalle:
node decideSearchStrategy
El decideSearchStrategy node analiza la entrada del usuario y determina si realizar una búsqueda centrada en la inversión o en el mercado. Utiliza un LLM con un esquema de salida estructurado (definido con Zod) para clasificar el tipo de consulta. Antes de tomar la decisión, recupera los filtros disponibles del índice mediante una agregación, lo que garantiza que el modelo cuente con información actualizada sobre sectores, ubicaciones y datos de financiación.
Para extraer los posibles valores de los filtros y enviarlos al LLM, usemos una consulta de agregación para obtenerlos directamente del índice de Elasticsearch. Esta lógica se encuentra en un método llamado getAvailableFilters:
async function getAvailableFilters() {
try {
const response = await esClient.search({
index: INDEX_NAME,
size: 0,
aggs: {
industries: {
terms: { field: "industry", size: 100 },
},
locations: {
terms: { field: "location", size: 100 },
},
funding_stages: {
terms: { field: "funding_stage", size: 20 },
},
business_models: {
terms: { field: "business_model", size: 10 },
},
lead_investors: {
terms: { field: "lead_investor", size: 100 },
},
funding_amount_stats: {
stats: { field: "funding_amount" },
},
},
});
return response.aggregations;
} catch (error) {
console.error("❌ Error getting available filters:", error);
return {};
}
}Con la consulta de agregación anterior, tenemos los siguientes resultados:
{
"industries": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "logistics",
"doc_count": 5
},
...
]
},
"locations": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "San Francisco, CA",
"doc_count": 4
},
{
"key": "New York, NY",
"doc_count": 3
},
...
]
},
"funding_stages": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "Series A",
"doc_count": 8
},
...
]
},
"business_models": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "B2B",
"doc_count": 13
},
...
]
},
"lead_investors": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "Battery Ventures",
"doc_count": 1
},
{
"key": "Benchmark Capital",
"doc_count": 1
},
...
]
},
"funding_amount_stats": {
"count": 20,
"min": 4500000,
"max": 35000000,
"avg": 14075000,
"sum": 281500000
}
}Vea todos los resultados aquí.
Para ambas estrategias, utilizaremos la búsqueda híbrida para detectar tanto la parte estructurada de la pregunta (filtros) como las partes más subjetivas (semántica). A continuación se muestra un ejemplo de ambas consultas utilizando plantillas de búsqueda:
await esClient.putScript({
id: INVESTMENT_FOCUSED_TEMPLATE,
script: {
lang: "mustache",
source: `{
"size": 5,
"retriever": {
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"semantic": {
"field": "semantic_field",
"query": "{{query_text}}"
}
}
}
},
{
"standard": {
"query": {
"bool": {
"filter": [
{"terms": {"funding_stage": {{#join}}{{#toJson}}funding_stage{{/toJson}}{{/join}}}},
{"range": {"funding_amount": {"gte": {{funding_amount_gte}}{{#funding_amount_lte}},"lte": {{funding_amount_lte}}{{/funding_amount_lte}}}}},
{"terms": {"lead_investor": {{#join}}{{#toJson}}lead_investor{{/toJson}}{{/join}}}},
{"range": {"monthly_revenue": {"gte": {{monthly_revenue_gte}}{{#monthly_revenue_lte}},"lte": {{monthly_revenue_lte}}{{/monthly_revenue_lte}}}}}
]
}
}
}
}
],
"rank_window_size": 100,
"rank_constant": 20
}
}
}`,
},
});Vea las consultas detalladas en el archivo elasticsearchSetup.ts . En el siguiente node, se decidirá cuál de las dos consultas se empleará:
// Node 1: Decide search strategy using LLM
async function decideSearchStrategy(state: typeof VCState.State) {
// Zod schema for specialized search strategy decision
const SearchDecisionSchema = z.object({
search_type: z
.enum(["investment_focused", "market_focused"])
.describe("Type of specialized search strategy to use"),
reasoning: z
.string()
.describe("Brief explanation of why this search strategy was chosen"),
});
const decisionLLM = llm.withStructuredOutput(SearchDecisionSchema);
// Get dynamic filters from Elasticsearch
const availableFilters = await getAvailableFilters();
const prompt = `Query: "${state.input}"
Available filters: ${JSON.stringify(availableFilters, null, 2)}
Choose between two specialized search strategies:
- investment_focused: For queries about funding stages, funding amounts, monthly revenue, lead investors, financial performance
- market_focused: For queries about industries, locations, business models, market segments, geographic markets
Analyze the query intent and choose the most appropriate strategy.
`;
try {
const result = await decisionLLM.invoke(prompt);
console.log(
`🤔 Search strategy: ${result.search_type} - ${result.reasoning}`
);
return {
searchStrategy: result.search_type,
};
} catch (error: any) {
console.error("❌ Error in decideSearchStrategy:", error.message);
return {
searchStrategy: "investment_focused",
};
}
}Nodes prepareInvestmentSearch y prepareMarketSearch
Ambos nodos emplean una función auxiliar compartida, extractFilterValues, que aprovecha el LLM para identificar los filtros relevantes mencionados en la entrada del usuario, como la industria, la ubicación, la etapa de financiación, el modelo de negocio, etc. Estamos utilizando este esquema para crear nuestra plantilla de búsqueda.
// Extract all possible filter values from user input
async function extractFilterValues(input: string) {
const FilterValuesSchema = z.object({
// Investment-focused filters
funding_stage: z
.array(z.string())
.default([])
.describe("Funding stage values mentioned in query"),
funding_amount_gte: z
.number()
.default(0)
.describe("Minimum funding amount in USD"),
funding_amount_lte: z
.number()
.default(100000000)
.describe("Maximum funding amount in USD"),
lead_investor: z
.array(z.string())
.default([])
.describe("Lead investor values mentioned in query"),
monthly_revenue_gte: z
.number()
.default(0)
.describe("Minimum monthly revenue in USD"),
monthly_revenue_lte: z
.number()
.default(10000000)
.describe("Maximum monthly revenue in USD"),
industry: z
.array(z.string())
.default([])
.describe("Industry values mentioned in query"),
location: z
.array(z.string())
.default([])
.describe("Location values mentioned in query"),
business_model: z
.array(z.string())
.default([])
.describe("Business model values mentioned in query"),
});
const extractorLLM = llm.withStructuredOutput(FilterValuesSchema);
const availableFilters = await getAvailableFilters();
const extractPrompt = `Extract ALL relevant filter values from: "${input}"
Available options: ${JSON.stringify(availableFilters, null, 2)}
Extract only values explicitly mentioned in the query. Leave fields empty if not mentioned.`;
return await extractorLLM.invoke(extractPrompt);
}Según de la intención detectada, el flujo de trabajo selecciona una de dos rutas:
prepareInvestmentSearch: desarrolla parámetros de búsqueda orientados a la financiación, incluyendo la etapa de financiación, el importe de la inversión, el inversionista y la información de renovación. Puedes encontrar la plantilla completa de consulta en el archivo elasticsearchSetup.ts :
// Node 2A: Prepare Investment-Focused Search Parameters
async function prepareInvestmentSearch(state: typeof VCState.State) {
console.log(
"💰 Preparing INVESTMENT-FOCUSED search parameters with financial emphasis..."
);
try {
// Extract all filter values from input
const values = await extractFilterValues(state.input);
let searchParams: any = {
template_id: INVESTMENT_FOCUSED_TEMPLATE,
query_text: state.input,
...values,
};
return { searchParams };
} catch (error) {
console.error("❌ Error preparing investment-focused params:", error);
return {
searchParams: {},
};
}
}prepareMarketSearch: crea parámetros orientados al mercado centrados en industrias, geografías y modelos de negocio. Ver la consulta completa en el archivo elasticsearchSetup.ts:
// Node 2B: Prepare Market-Focused Search Parameters
async function prepareMarketSearch(state: typeof VCState.State) {
console.log(
"🔍 Preparing MARKET-FOCUSED search parameters with market emphasis..."
);
try {
// Extract all filter values from input
const values = await extractFilterValues(state.input);
let searchParams: any = {
template_id: MARKET_FOCUSED_TEMPLATE,
query_text: state.input,
...values,
};
return { searchParams };
} catch (error) {
console.error("❌ Error preparing market-focused params:", error);
return {};
}
}Node executeSearch
Este node toma los parámetros de búsqueda generados del estado y los envía primero a Elasticsearch, usando la _render API para visualizar la consulta con fines de depuración, y luego envía una petición para recuperar los resultados.
// Node 3: Execute Search
async function executeSearch(state: typeof VCState.State) {
const { searchParams } = state;
try {
// getting formed query from template for debugging
const renderedTemplate = await esClient.renderSearchTemplate({
id: searchParams.template_id,
params: searchParams,
});
console.log(
"📋 Complete query:",
JSON.stringify(renderedTemplate.template_output, null, 2)
);
const results = await esClient.searchTemplate({
index: INDEX_NAME,
id: searchParams.template_id,
params: searchParams,
});
return {
results: results.hits.hits.map((hit: any) => hit._source),
};
} catch (error: any) {
console.error(`❌ ${state.searchParams.search_type} search error:`, error);
return { results: [] };
}
}node visualizarResultados
Finalmente, este nodo muestra los resultados de Elasticsearch.
// Node 4: Visualize results
async function visualizeResults(state: typeof VCState.State) {
const results = state.results || [];
let formattedResults = `🎯 Found ${results.length} startups matching your criteria:\n\n`;
results.forEach((startup: any, index: number) => {
formattedResults += `${index + 1}. **${startup.company_name}**\n`;
formattedResults += ` 📍 ${startup.location} | 🏢 ${startup.industry} | 💼 ${startup.business_model}\n`;
formattedResults += ` 💰 ${startup.funding_stage} - $${(
startup.funding_amount / 1000000
).toFixed(1)}M\n`;
formattedResults += ` 👥 ${startup.employee_count} employees | 📈 $${(
startup.monthly_revenue / 1000
).toFixed(0)}K MRR\n`;
formattedResults += ` 🏦 Lead: ${startup.lead_investor}\n`;
formattedResults += ` 📝 ${startup.description}\n\n`;
});
return {
final: formattedResults,
};
}Programáticamente, todo el grafo se ve así:
const workflow = new StateGraph(VCState)
// Register nodes - these are the processing functions
.addNode("decideStrategy", decideSearchStrategy)
.addNode("prepareInvestment", prepareInvestmentSearch)
.addNode("prepareMarket", prepareMarketSearch)
.addNode("executeSearch", executeSearch)
.addNode("visualizeResults", visualizeResults)
// Define execution flow with conditional branching
.addEdge(START, "decideStrategy") // Start with strategy decision
.addConditionalEdges(
"decideStrategy",
(state: typeof VCState.State) => state.searchStrategy, // Conditional function
{
investment_focused: "prepareInvestment", // If investment focused -> RRF template preparation
market_focused: "prepareMarket", // If market focused -> dynamic query preparation
}
)
.addEdge("prepareInvestment", "executeSearch") // Investment prep -> execute
.addEdge("prepareMarket", "executeSearch") // Market prep -> execute
.addEdge("executeSearch", "visualizeResults") // Execute -> visualize
.addEdge("visualizeResults", END); // End workflowComo puede ver, tenemos una aplicación condicional donde la aplicación decide qué “ruta” o node ejecutar a continuación. Esta característica es útil cuando los flujos de trabajo necesitan lógica de ramificación, como elegir entre múltiples herramientas o incluir un paso con intervención de una persona.
Con las características básicas del núcleo de LangGraph entendidas, podemos configurar la aplicación donde se ejecutará el código:
Juntando todo en un flujo de trabajo main, aquí declaramos el grafo con todos los elementos bajo la variable flujo de trabajo:
async function main() {
await createIndex();
await createSearchTemplates();
await ingestDocuments();
// Create the workflow graph with shared state
const workflow = new StateGraph(VCState)
// Register nodes - these are the processing functions
.addNode("decideStrategy", decideSearchStrategy)
.addNode("prepareInvestment", prepareInvestmentSearch)
.addNode("prepareMarket", prepareMarketSearch)
.addNode("executeSearch", executeSearch)
.addNode("visualizeResults", visualizeResults)
// Define execution flow with conditional branching
.addEdge(START, "decideStrategy") // Start with strategy decision
.addConditionalEdges(
"decideStrategy",
(state: typeof VCState.State) => state.searchStrategy, // Conditional function
{
investment_focused: "prepareInvestment", // If investment focused -> RRF template preparation
market_focused: "prepareMarket", // If market focused -> dynamic query preparation
}
)
.addEdge("prepareInvestment", "executeSearch") // Investment prep -> execute
.addEdge("prepareMarket", "executeSearch") // Market prep -> execute
.addEdge("executeSearch", "visualizeResults") // Execute -> visualize
.addEdge("visualizeResults", END); // End workflow
const app = workflow.compile();
await saveGraphImage(app);
const query =
"Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K";
const marketResult = await app.invoke({ input: query });
console.log(marketResult.final);
}La variable de consulta simula la entrada del usuario introducida en una barra de búsqueda hipotética:

De la frase en lenguaje natural “Encuentra startups con financiamiento de Serie A o Serie B entre $8M y $25M, e ingresos mensuales superiores a $500K” se extraerán todos los filtros.
Finalmente, invoca el método principal:
main().catch(console.error);Resultados
🔍 Checking if index exists...
🏗️ Creating index...
✅ Index created successfully!
Ingesting documents...
✅ Documents ingested successfully!
✅ Investment-focused template created successfully!
✅ Market-focused template created successfully!
📊 Workflow graph saved as: ./workflow_graph.png
🔍 Query: "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K"
🤔 Search strategy: investment_focused - The query specifically seeks profitable fintech startups with defined funding amounts and high monthly revenue, which aligns closely with financial performance metrics and investment-related criteria.
💰 Preparing INVESTMENT-FOCUSED search parameters with financial emphasis...
📋 Complete query: {
"size": 5,
"retriever": {
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"semantic": {
"field": "semantic_field",
"query": "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K"
}
}
}
},
{
"standard": {
"query": {
"bool": {
"filter": [
{
"terms": {
"funding_stage": [
"Series A",
"Series B"
]
}
},
{
"range": {
"funding_amount": {
"gte": 8000000,
"lte": 25000000
}
}
},
{
"terms": {
"lead_investor": []
}
},
{
"range": {
"monthly_revenue": {
"gte": 500000,
"lte": 0
}
}
}
]
}
}
}
}
],
"rank_window_size": 100,
"rank_constant": 20
}
}
}
🎯 Found 5 startups matching your criteria:
1. **TechFlow**
📍 San Francisco, CA | 🏢 logistics | 💼 B2B
💰 Series A - $8.0M
👥 45 employees | 📈 $500K MRR
🏦 Lead: Sequoia Capital
📝 TechFlow optimizes supply chain operations using AI-powered route optimization and real-time tracking. Founded in 2023, shows remarkable growth with $500K monthly revenue.
2. **DataViz**
📍 New York, NY | 🏢 enterprise software | 💼 B2B
💰 Series A - $10.0M
👥 42 employees | 📈 $450K MRR
🏦 Lead: Battery Ventures
📝 DataViz creates intuitive data visualization tools for enterprise customers. No-code platform allows business users to create dashboards without technical expertise.
3. **FinanceAI**
📍 San Francisco, CA | 🏢 fintech | 💼 B2C
💰 Series C - $25.0M
👥 120 employees | 📈 $1200K MRR
🏦 Lead: Tiger Global Management
📝 FinanceAI provides AI-powered investment advisory services to retail investors. Uses machine learning to analyze market trends with over 100,000 active users.
4. **UrbanMobility**
📍 New York, NY | 🏢 logistics | 💼 B2B2C
💰 Series B - $15.0M
👥 78 employees | 📈 $750K MRR
🏦 Lead: Kleiner Perkins
📝 UrbanMobility revolutionizes urban transportation through autonomous delivery drones and smart logistics hubs. Partners with major retailers for same-day delivery across Manhattan and Brooklyn.
5. **HealthTech Solutions**
📍 Boston, MA | 🏢 healthcare | 💼 B2B
💰 Series B - $18.0M
👥 95 employees | 📈 $900K MRR
🏦 Lead: General Catalyst
📝 HealthTech Solutions develops medical devices and software for remote patient monitoring. Comprehensive telehealth platform reducing hospital readmissions by 30%.
✨ Done in 18.80s.Para la entrada enviada, la aplicación elige la ruta centrada en la inversión y, como resultado, podemos ver la consulta de Elasticsearch generada por el flujo de trabajo de LangGraph, que extrae los valores y los rangos de la entrada del usuario. También podemos ver la consulta enviada a Elasticsearch con los valores extraídos aplicados y, finalmente, los resultados formateados por el node visualizeResults con los resultados.
Ahora vamos a probar el node centrado en el mercado usando la consulta “Encuentre startups de fintech y salud en San Francisco, Nueva York o Boston”:
...
🔍 Query: Find fintech and healthcare startups in San Francisco, New York, or Boston
🤔 Search strategy: market_focused - The query is focused on finding fintech startups in San Francisco that are disrupting traditional banking and payment systems, which pertains to specific industries (fintech) and locations (San Francisco). Thus, a market-focused strategy is more appropriate.
🔍 Preparing MARKET-FOCUSED search parameters with market emphasis...
📋 Complete query: {
"size": 5,
"retriever": {
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"semantic": {
"field": "semantic_field",
"query": "Find fintech and healthcare startups in San Francisco, New York, or Boston"
}
}
}
},
{
"standard": {
"query": {
"bool": {
"filter": [
{
"terms": {
"industry": [
"fintech",
"healthcare"
]
}
},
{
"terms": {
"location": [
"San Francisco, CA",
"New York, NY",
"Boston, MA"
]
}
},
{
"terms": {
"business_model": []
}
}
]
}
}
}
}
],
"rank_window_size": 50,
"rank_constant": 10
}
}
}
🎯 Found 5 startups matching your criteria:
1. **FinanceAI**
📍 San Francisco, CA | 🏢 fintech | 💼 B2C
💰 Series C - $25.0M
👥 120 employees | 📈 $1200K MRR
🏦 Lead: Tiger Global Management
📝 FinanceAI provides AI-powered investment advisory services to retail investors. Uses machine learning to analyze market trends with over 100,000 active users.
2. **CryptoWallet**
📍 Miami, FL | 🏢 fintech | 💼 B2C
💰 Series B - $16.0M
👥 73 employees | 📈 $820K MRR
🏦 Lead: Coinbase Ventures
📝 CryptoWallet provides secure digital wallet solutions for cryptocurrency trading and storage. Multi-chain support with enterprise-grade security features.
...
✨ Done in 7.41s.Aprendizajes
Durante el proceso de escritura aprendí:
Debemos mostrar al LLM los valores exactos de los filtros; de lo contrario, dependemos de que el usuario escriba los valores exactos de las cosas. Para baja cardinalidad, este enfoque está bien, pero cuando la cardinalidad es alta, necesitamos algún mecanismo para filtrar los resultados.
El uso de plantillas de búsqueda hace que los resultados sean mucho más consistentes que dejar que el LLM escriba la consulta de Elasticsearch, y también es más rápido.
Los bordes condicionales son un mecanismo potente para crear aplicaciones con múltiples variantes y rutas de ramificación.
La salida estructurada es extremadamente útil cuando se genera información con LLM porque impone respuestas predecibles y de tipo seguro. Esto mejora la confiabilidad y reduce las interpretaciones incorrectas de los prompts.
La combinación de búsqueda semántica y estructurada a través de la recuperación híbrida produce resultados mejores y más relevantes, equilibrando la precisión y la comprensión del contexto.
Conclusión
En este ejemplo, combinamos LangGraph.js con Elasticsearch para crear un flujo de trabajo dinámico capaz de interpretar consultas en lenguaje natural y decidir entre estrategias de búsqueda financieras u orientadas al mercado. Este enfoque reduce la complejidad de la creación de consultas manuales, al tiempo que mejora la flexibilidad y la precisión para los analistas de capital de riesgo.




