Articles de blog

Créez un workflow de recherche IA financière avec LangGraph.js et Elasticsearch

Apprenez à utiliser LangGraph.js avec Elasticsearch pour créer un workflow de recherche financière alimenté par l'IA qui transforme les requêtes en langage naturel en filtres dynamiques et conditionnels pour l'analyse des investissements et du marché.

La création d'applications de recherche IA implique souvent la coordination de plusieurs tâches, la récupération et l'extraction de données dans un workflow fluide. LangGraph simplifie ce processus en permettant aux développeurs d'orchestrer les agents d'IA à l'aide d'une structure basée sur des nodes. Dans cet article, nous allons construire une solution financière en utilisant LangGraph.js.

Qu'est-ce que LangGraph ?

LangGraph est un framework pour construire des agents d’IA et les orchestrer dans un workflow afin de créer des applications assistées par l’IA. LangGraph dispose d’une architecture de nodes où nous pouvons déclarer des fonctions représentant des tâches et les assigner comme nodes du workflow. Le résultat de l'interaction de plusieurs nodes sera un graphe. LangGraph fait partie du LangChain écosystème plus large, qui fournit des outils pour construire des systèmes d'IA modulaires et composables.

Pour mieux comprendre l’utilité de LangGraph, résolvons une situation problématique en l’utilisant.

Aperçu de la solution

Dans une société de capital-risque, les investisseurs ont accès à une vaste base de données avec de nombreuses options de filtrage, mais lorsqu'ils veulent combiner des critères, cela devient difficile et lent. Il se peut donc que certaines start-ups pertinentes ne soient pas trouvées pour l'investissement. Cela conduit à passer beaucoup de temps à essayer d'identifier les meilleurs candidats, voire à perdre des opportunités.

Avec LangGraph et Elasticsearch, vous pouvez effectuer des recherches filtrées en utilisant le langage naturel, ce qui évite aux utilisateurs de devoir construire manuellement des requêtes complexes avec des dizaines de filtres. Pour plus de flexibilité, le workflow choisit automatiquement, en fonction de l'entrée de l'utilisateur, entre deux types de requêtes :

  • Requêtes d’investissement : elles visent les données financières et de financement des start-up, notamment les tours de table, la valorisation ou le CA. Exemple : « Trouvez des startups avec un financement de série A ou série B entre 8 millions et 25 millions de dollars et un chiffre d’affaires mensuel supérieur à 500 000 $. »

  • Requêtes axées sur le marché: elles se concentrent sur les secteurs d’activité, les marchés géographiques ou les modèles économiques, en aidant à identifier des opportunités dans des secteurs ou régions spécifiques. Exemple : « Trouvez des startups de la fintech et de la santé à San Francisco, New York ou Boston. »

Pour garantir la robustesse des requêtes, nous allons faire en sorte que le LLM génère des modèles de recherche au lieu de requêtes DSL complètes. De cette façon, vous obtenez toujours la requête souhaitée, et le LLM n'a qu'à compléter les informations manquantes sans avoir à élaborer la requête dont vous avez besoin à chaque fois.

Ce dont vous avez besoin pour commencer

  • Clé API Elasticsearch

  • Clé d'API OpenAPI

  • Node 18 ou version ultérieure

Instructions étape par étape

Dans cette section, voyons comment l'application sera présentée. Nous utiliserons TypeScript, un sur-ensemble de JavaScript qui ajoute des types statiques. Cela rend le code plus fiable, plus facile à maintenir et plus sûr en détectant les erreurs dès le début, tout en assurant une compatibilité totale avec JavaScript.

Le flux des nœuds se présentera comme suit :

L'image ci-dessus est générée par LangGraph et représente le workflow qui définit l'ordre d'exécution et la logique conditionnelle entre les nodes :

  • decideStrategy : utilise un LLM pour rechercher la requête de l'utilisateur et choisir entre deux stratégies de recherche spécialisées, axée sur l'investissement ou axée sur le marché.

  • PrepareInvestmentSearch : extrait les valeurs de filtre de la requête et crée un modèle prédéfini mettant l'accent sur les paramètres financiers et liés au financement.

  • PrepareMarketSearch: extrait également les valeurs des filtres, mais crée dynamiquement des paramètres en mettant l'accent sur le marché, le secteur et le contexte géographique.

  • ExecuteSearch : envoie la recherche construite à Elasticsearch à l'aide d'un modèle de recherche et extrait les documents de démarrage correspondants.

  • VisualiserResults : met en forme les résultats finaux sous la forme d'un résumé clair et lisible présentant les principaux attributs de la start-up tels que le financement, le secteur d'activité et le chiffre d'affaires.

Ce flux comprend un branchement conditionnel, fonctionnant comme une instruction « si », qui détermine s'il faut rechercher le chemin d'investissement ou de recherche de marché en fonction de l'entrée de l'utilisateur. Cette logique de décision, pilotée par le LLM, rend le workflow adaptatif et conscient du contexte, un mécanisme que nous explorerons plus en détail dans les sections suivantes.

État de LangGraph

Avant de voir chaque node individuellement, nous devons comprendre comment les nodes communiquent et partagent les données. Pour cela, LangGraph nous permet de définir l'état du workflow. Cela définit l'état partagé qui sera transmis entre les nodes.

L’état agit comme un conteneur partagé stockant les données intermédiaires du workflow : il enregistre d’abord la requête en langage naturel de l’utilisateur, puis la stratégie de recherche choisie, les paramètres prêts pour Elasticsearch, les résultats de recherche et, pour finir, le résultat formaté.

Cette architecture permet à chaque nœud de lire et de modifier l’état, ce qui garantit un flux d’informations constant, de l’entrée de l’utilisateur jusqu’à la visualisation finale.

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
});

Configurer l'application

Tout le code de cette section se trouve dans le dépôt elasticsearch-labs.

Dans le dossier où l’application sera installée, ouvrez un terminal et initialisez une application Node.js avec la commande :

npm init -y

Nous pouvons maintenant installer les dépendances nécessaires à ce projet :

npm install @elastic/elasticsearch @langchain/langgraph @langchain/openai @langchain/core dotenv zod && npm install --save-dev @types/node tsx typescript
  • @elastic/elasticsearch: Permet de gérer les requêtes Elasticsearch, comme l’ingestion et la récupération des données.

  • @langchain/langgraph: Dépendance JS pour fournir tous les outils LangGraph.

  • @langchain/openai: Client OpenAI LLM pour LangChain.

  • @langchain/core : Offre les composantes de base essentielles aux applications LangChain, notamment les modèles d’invite.

  • dotenv: Dépendance nécessaire pour utiliser les variables d'environnement en JavaScript.

  • zod: Dépendance au type de données.

@types/node tsx typescript nous permet d'écrire et d'exécution du code TypeScript.

Créez maintenant les fichiers suivants :

  • elasticsearchSetup.ts: Créera les mapping d'index, chargera les données à partir d'un fichier JSON, et ingérera les données dans Elasticsearch.

  • main.ts: inclura l’application LangGraph.

  • .env: fichier pour stocker les variables d’environnement

Dans le fichier .env, ajoutons les variables d’environnement suivantes :

ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"

La clé APIK de l'OpenAPI ne sera pas utilisée directement dans le code ; elle sera utilisée en interne par la bibliothèque @langchain/openai.

Toute la logique concernant la création de mappages, la création de modèles de recherche et l’ingestion des ensembles de données se trouve dans le fichier elasticsearchSetup.ts. Dans les prochaines étapes, nous nous concentrerons sur le fichier main.ts. Vous pouvez également consulter l'ensemble de données pour mieux comprendre l'aspect des données sur le site dataset.json.

Application LangGraph

Dans le fichier main.ts, importons certaines dépendances nécessaires pour consolider l'application LangGraph. Dans ce fichier, vous devez également inclure les fonctions node et la déclaration d’état. La déclaration du graphe sera effectuée dans une méthode main dans les prochaines étapes. Le fichier elasticsearchSetup.ts contiendra les aides Elasticsearch que nous allons utiliser dans les Nodes dans les étapes suivantes.

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" });

Ainsi que nous l’avons vu, le client LLM sera mobilisé pour générer les paramètres du modèle de recherche Elasticsearch en fonction de la question de l’utilisateur.

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);
  }
}

La méthode ci-dessus génère l'image du graphe au format png et utilise l'API Mermaid.INK en arrière-plan. Ceci est utile si vous souhaitez voir comment les nodes de l'application interagissent dans le cadre d'une visualisation stylisée.

Nodes LangGraph

À présent, voyons chaque node en détail :

Node decideSearchStrategy

Le node decideSearchStrategy analyse les entrées de l'utilisateur et détermine s'il convient d'effectuer une rechercher axée sur les investissements ou axée sur le marché. Il utilise un LLM avec un schéma de sortie structuré (défini avec Zod) pour classer le type de requête. Avant de prendre la décision, il récupère les filtres disponibles de l'index en utilisant une agrégation, en garantissant que le modèle dispose d'un contexte à jour sur les industries, les localisations et les données de financement.

Pour extraire les valeurs possibles des filtres et les envoyer au LLM, utilisons une requête d'agrégation pour les récupérer directement depuis l'index Elasticsearch. Cette logique est allouée dans une méthode appelée 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 {};
  }
}

Avec la requête d'agrégation ci-dessus, nous avons les résultats suivants :

{
  "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
  }
}

Découvrez tous les résultats ici.

Pour les deux stratégies, nous allons utiliser la recherche hybride afin de détecter à la fois la partie structurée de la question (filtres) et les parties plus subjectives (sémantique). Voici un exemple des deux requêtes utilisant des modèles de recherche :

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
            }
          }
        }`,
      },
    });

Regardez les requêtes détaillées dans le fichier elasticsearchSetup.ts . Dans le node suivant, il sera décidé laquelle des deux requêtes sera utilisée :

// 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 et prepareMarketSearch

Les deux nœuds utilisent une fonction d’assistance partagée, extractFilterValues, qui exploite le LLM pour identifier les filtres pertinents mentionnés dans les entrées de l’utilisateur, tels que l’industrie, la localisation, le stade de financement, le modèle économique, etc. Nous utilisons ce schéma pour construire notre modèle de recherche.

// 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);
}

Selon l'intention détectée, le workflow sélectionne l'un des deux chemins :

prepareInvestmentSearch : définit des paramètres de rechercher orientés sur la finance, notamment l''étape du financement, le montant du financement, les informations relatives à l''investisseur et au renouvellement. Vous pouvez trouver le modèle complet de requête dans le fichier 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 : crée des paramètres orientés vers le marché, axés sur les industries, les régions géographiques et les modèles économiques. Voir l’intégralité de la requête dans le fichier 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

Ce node prend les paramètres de rechercher générés à partir de l'état et les envoie d'abord à Elasticsearch, en utilisant l'API _render pour visualiser la requête à des fins de débogage, puis envoie une demande pour récupérer les résultats.

// 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 visualizeResults

Enfin, ce node affiche les résultats d’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,
  };
}

Par programmation, l'ensemble du graphe ressemble à ceci :

  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

Comme vous pouvez le constater, nous avons une arête conditionnelle où l'application décide quel « chemin » ou node sera exécuté ensuite. Cette fonctionnalité est utile lorsque les workflows nécessitent une logique de branchement, comme le choix entre plusieurs outils ou l’inclusion d’une étape humaine dans la boucle.

Maintenant que vous maîtrisez les fonctionnalités clés de LangGraph, nous pouvons préparer l’application qui exécutera le code :

Rassemblons tout dans une méthode main , ici nous déclarons le graphe avec tous les éléments sous la variable workflow :

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 requête simule l'entrée utilisateur saisie dans une barre de recherche hypothétique :

D’après la phrase en langage naturel « Trouvez des startups avec un financement de la série A ou de la série B entre 8 millions et 25 millions de dollars et un chiffre d’affaires mensuel supérieur à 500 000 $ », tous les filtres seront extraits.

Enfin, invoquez la méthode principale :

main().catch(console.error);

Résultats

🔍 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.

Pour l'entrée envoyée, l'application choisit le chemin axé sur l'investissement et, par conséquent, nous pouvons voir la requête Elasticsearch générée par le workflow, qui extrait les valeurs et les plages de l'entrée de l'utilisateur. Nous pouvons également voir la requête envoyée à Elasticsearch avec les valeurs extraites appliquées, et enfin, les résultats formatés par le nœud visualizeResults avec les résultats.

Testons maintenant le node axé sur le marché en utilisant la requête « Trouver des startups fintech et de la santé à San Francisco, New York ou 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.

Enseignements

Pendant le processus d'écriture, j'ai appris :

  • Nous devons montrer au LLM les valeurs exactes des filtres, sinon nous attendons de l'utilisateur qu'il saisisse les valeurs exactes des éléments. Pour une faible cardinalité, cette approche convient, mais lorsque la cardinalité est élevée, nous avons besoin d'un mécanisme pour filtrer les résultats

  • Utiliser des modèles de recherche rend les résultats bien plus cohérents que de laisser le LLM écrire la requête Elasticsearch, et c’est aussi plus rapide

  • Les arêtes conditionnelles constituent un mécanisme puissant pour construire des applications avec de multiples variantes et chemins de branchement.

  • La sortie structurée est extrêmement utile lors de la génération d'informations avec des LLM, car elle applique des réponses prévisibles et sécurisées. Cela améliore la fiabilité et réduit les erreurs d'interprétation.

La combinaison de la recherche sémantique et de la recherche structurée par le biais d'une recherche hybride produit des résultats meilleurs et plus pertinents, en équilibrant précision et compréhension du contexte.

Conclusion

Dans cet exemple, nous combinons LangGraph.js avec Elasticsearch pour créer un workflow dynamique capable d'interpréter les requêtes en langage naturel et de décider entre des stratégies de recherche axées sur la finance ou le marché. Cette approche réduit la complexité de la création de requêtes manuelles tout en améliorant la flexibilité et la précision pour les analystes en capital-risque.

Pour aller plus loin

Décrivez, ne dessinez pas : tableaux de bord Kibana IA natifs via MCP et ES|QL

Prêt à créer des expériences de recherche d'exception ?

Une recherche suffisamment avancée ne se fait pas avec les efforts d'une seule personne. Elasticsearch est alimenté par des data scientists, des ML ops, des ingénieurs et bien d'autres qui sont tout aussi passionnés par la recherche que vous. Mettons-nous en relation et travaillons ensemble pour construire l'expérience de recherche magique qui vous permettra d'obtenir les résultats que vous souhaitez.

Jugez-en par vous-même