signals

Par posthog · skills

Comment interroger la table `document_embeddings` pour récupérer des données de signal brutes via HogQL. À utiliser lorsque vous devez effectuer une recherche sémantique sur des signaux, récupérer tous les signaux ayant contribué à un rapport spécifique, ou lister les types de signaux. Pour parcourir la couche de rapports organisés (l'Inbox) — lister des rapports, filtrer par statut/source, explorer un rapport individuel par ID — utilisez d'abord le skill `inbox-exploration` ; basculez vers ce skill ensuite si l'utilisateur souhaite accéder aux observations sous-jacentes.

npx skills add https://github.com/posthog/skills --skill signals

Interrogation des Signaux

Qu'est-ce que les Signaux ?

Les Signaux sont des observations automatisées que PostHog génère en surveillant les données produit d'un client à travers plusieurs sources — suivi d'erreurs, web analytics, expériences, session replay, et plus. Chaque signal est une description en langage naturel concis décrivant quelque chose de notable (par exemple « Le taux d'erreur a augmenté 3× sur /checkout »).

Les Signaux sont regroupés en Rapports de Signaux. Quand un rapport accumule suffisamment de poids, il est résumé et évalué pour son caractère actionnable. Un rapport de signal représente un cluster d'observations connexes qui décrivant ensemble un problème ou une tendance significative.

Les Signaux et leurs embeddings sont stockés dans la table ClickHouse document_embeddings, interrogeable via HogQL par l'intermédiaire de l'outil MCP posthog:execute-sql. Ils peuvent fournir un moyen utile d'interroger sémantiquement les événements récents qui se sont produits dans le produit de l'utilisateur.

Quand utiliser cette compétence par rapport à inbox-exploration

Les deux compétences couvrent des couches différentes du même produit :

  • inbox-exploration — couche de rapports curés via des outils MCP dédiés (inbox-reports-list, inbox-reports-retrieve, inbox-source-configs-list, inbox-source-configs-retrieve). À utiliser pour « qu'y a-t-il dans ma boîte de réception ? », « qu'est-ce qui est actionnable ? », filtrer les rapports par statut / source / relecteur suggéré, chercher un rapport spécifique par ID ou URL.
  • Cette compétence (signals) — couche de signaux bruts via HogQL sur document_embeddings. À utiliser quand la couche de rapports curés n'est pas suffisante : recherche sémantique sur le texte de signal, récupération de tous les signaux qui ont contribué à un rapport spécifique, listing des types de signaux existants, ou toute analytique ad hoc que les outils de rapport n'exposent pas.

Le pattern typique est de commencer avec inbox-exploration, obtenir un report_id ou une notion de la zone qui intéresse l'utilisateur, puis basculer vers cette compétence quand l'utilisateur veut voir les observations brutes.

Référence des Colonnes et de la Table

L'alias de table HogQL est document_embeddings. HogQL contraint automatiquement les requêtes à l'équipe actuelle — vous n'avez jamais besoin de filtrer sur team_id. Colonnes clés pour les signaux :

Colonne Type Description
product String Bucket produit — toujours 'signals' pour les signaux
document_type String Type de document — toujours 'signal' pour les signaux
model_name String Modèle d'embedding — toujours 'text-embedding-3-small-1536'
document_id String ID de signal unique (UUID)
timestamp DateTime64(3) Quand le signal a été créé
inserted_at DateTime64(3) Quand cette version de ligne a été insérée (utilisée pour déduplication et soft delete)
content String Le texte de description du signal
metadata String String JSON avec report_id, info source, poids, flag suppression, etc
embedding Array(Float64) Vecteur embedding de 1536 dimensions

Filtres Obligatoires

Chaque requête de signaux DOIT inclure tous ces quatre filtres. Manquer l'un d'eux peut causer l'échec de la requête avec une erreur de modèle invalide, retourner des mauvaises données, ou déclencher des scans inutilement coûteux :

WHERE model_name = 'text-embedding-3-small-1536'
  AND product = 'signals'
  AND document_type = 'signal'
  AND timestamp >= now() - INTERVAL 30 DAY

Le filtre model_name est particulièrement critique — le moteur HogQL l'utilise pour router vers la bonne table ClickHouse sous-jacente. Si le filtre d'égalité WHERE model_name = ... est manquant ou utilise un modèle inconnu, la requête échouera avec une erreur « Invalid model name » (vous ne pouvez pas utiliser IN ou autres expressions ici).

Les filtres product et document_type sont tout aussi importants — le même modèle contient les données de plusieurs produits (par ex. suivi d'erreurs, mémoire IA). Sans ces filtres vous obtiendrez des données non liées mélangées.

Le filtre timestamp est requis pour la performance — la table est partitionnée par semaine et a une TTL de 3 mois. Incluez toujours une borne temporelle en utilisant now() - INTERVAL N DAY (ou WEEK, MONTH, etc.). Défaut à 30 jours à moins que vous ayez une raison de regarder plus loin. Généralement, les données plus récentes sont plus susceptibles d'être pertinentes, sauf si vous enquêtez sur un problème de longue date.

Pattern de Déduplication

La table sous-jacente peut contenir plusieurs versions du même signal (par ex. après une réémission suite à un soft-delete). Vous DEVEZ toujours dédupliquer en wrappant les lectures dans une sous-requête utilisant argMax(..., inserted_at) groupée par document_id.

Note : HogQL supporte l'accès par point metadata.field_name sur la colonne brute metadata JSON, mais l'information de type est perdue quand la colonne passe à travers les fonctions d'agrégation comme argMax(). Vous DEVEZ extraire les champs metadata individuels dans la sous-requête interne de dédup — ne passez PAS le blob metadata entier à travers argMax et n'y accédez par point dans la requête externe, car cela échouera avec une erreur de type.

L'accès JSON par point de HogQL extrait toujours les valeurs comme Nullable(String), peu importe le type JSON sous-jacent. Cela signifie que metadata.deleted est la string 'true'/'false'/null, pas un Bool. Utilisez deleted != 'true' — n'utilisez PAS NOT deleted.

SELECT ... FROM (
    SELECT
        document_id,
        argMax(content, inserted_at) as content,
        argMax(metadata.report_id, inserted_at) as report_id,
        argMax(metadata.source_product, inserted_at) as source_product,
        argMax(metadata.source_type, inserted_at) as source_type,
        argMax(metadata.deleted, inserted_at) as deleted,
        argMax(embedding, inserted_at) as embedding,
        argMax(timestamp, inserted_at) as signal_ts
    FROM document_embeddings
    WHERE model_name = 'text-embedding-3-small-1536'
      AND product = 'signals'
      AND document_type = 'signal'
      AND timestamp >= now() - INTERVAL 1 MONTH
    GROUP BY document_id
)
WHERE deleted != 'true'

Ne sélectionnez la colonne embedding dans la sous-requête interne que quand vous en avez vraiment besoin pour les recherches par similarité — c'est un tableau de 1536 floats et coûteux à matérialiser autrement.

La Fonction embedText()

embedText() est une fonction HogQL qui convertit une string texte en vecteur embedding au moment de la compilation de la requête. Elle appelle l'API d'embedding et insère le vecteur résultant comme constante avant d'exécuter la requête. Cela signifie que vous pouvez faire une recherche sémantique dans une seule requête sans étape externe d'embedding.

Signature : embedText(text, model_name)

  • text — la string à embedder. Doit être un littéral string, pas une référence de colonne.
  • model_name — le modèle d'embedding à utiliser. Pour les signaux, utilisez toujours 'text-embedding-3-small-1536'.

Les deux arguments doivent être des strings littérales. Vous ne pouvez pas passer de valeurs colonnes ou d'expressions — la fonction se résout au moment de la compilation, pas par ligne.

cosineDistance() pour la Recherche par Similarité

Utilisez cosineDistance(embedding, ...) pour classer les signaux par similarité sémantique. Des valeurs plus basses = plus similaires. Toujours ORDER BY distance ASC et ajoutez un LIMIT.

cosineDistance(embedding, embedText('your search text', 'text-embedding-3-small-1536')) as distance

Le modèle d'embedding (text-embedding-3-small-1536) utilise matryoshka representation learning, donc les dimensions d'embedding sont ordonnées par importance. Cela signifie que la recherche par similarité fonctionne bien même à haute dimensionnalité — la malédiction de la dimensionnalité n'est pas une préoccupation significative ici.

Champs JSON de Metadata

La colonne metadata est une string JSON. HogQL supporte l'accès par point metadata.field_name uniquement sur la colonne brute de table. Après agrégation (par ex. argMax), le type JSON est perdu et l'accès par point échouera. Extrayez toujours les champs dont vous avez besoin dans la sous-requête de dédup.

Champ Accès requête interne Description
report_id metadata.report_id UUID du Rapport de Signal parent (vide s'il n'est pas assigné)
source_product metadata.source_product Produit d'origine (utilisez Exemple 3 pour découvrir les valeurs disponibles)
source_type metadata.source_type Type de signal (utilisez Exemple 3 pour découvrir les valeurs disponibles)
source_id metadata.source_id ID de l'entité source
weight metadata.weight Poids du signal (contribue au seuil de promotion du rapport)
deleted metadata.deleted Flag soft-deletion (extrait comme String — comparez avec != 'true')
extra metadata.extra Blob JSON arbitraire du produit source
match_metadata metadata.match_metadata Raisonnement LLM match stocké durant le grouping

Exemple 1 : Recherche Sémantique de Signaux

Trouvez les signaux les plus similaires à une requête en langage naturel. C'est la requête la plus utile pour comprendre ce qui se passe dans le produit d'un client :

SELECT
    document_id,
    content,
    report_id,
    source_product,
    source_type,
    cosineDistance(embedding, embedText('users seeing errors on checkout page', 'text-embedding-3-small-1536')) as distance
FROM (
    SELECT
        document_id,
        argMax(content, inserted_at) as content,
        argMax(metadata.report_id, inserted_at) as report_id,
        argMax(metadata.source_product, inserted_at) as source_product,
        argMax(metadata.source_type, inserted_at) as source_type,
        argMax(metadata.deleted, inserted_at) as deleted,
        argMax(embedding, inserted_at) as embedding,
        argMax(timestamp, inserted_at) as signal_ts
    FROM document_embeddings
    WHERE model_name = 'text-embedding-3-small-1536'
      AND product = 'signals'
      AND document_type = 'signal'
      AND timestamp >= now() - INTERVAL 1 MONTH
    GROUP BY document_id
)
WHERE deleted != 'true'
ORDER BY distance ASC
LIMIT 10

Adaptez le premier argument de embedText à ce que vous recherchez. Écrivez-le comme une description en langage naturel du type de problème ou d'observation que vous voulez trouver.

Pour restreindre aux signaux qui ont déjà été groupés dans un rapport, ajoutez AND report_id != '' au WHERE externe.

Exemple 2 : Récupérer Tous les Signaux pour un Rapport Spécifique

Une fois que vous avez un report_id (d'une recherche sémantique ou de l'API Signal Reports), récupérez tous les signaux appartenant à ce rapport :

SELECT
    document_id,
    content,
    report_id,
    source_product,
    source_type,
    signal_ts
FROM (
    SELECT
        document_id,
        argMax(content, inserted_at) as content,
        argMax(metadata.report_id, inserted_at) as report_id,
        argMax(metadata.source_product, inserted_at) as source_product,
        argMax(metadata.source_type, inserted_at) as source_type,
        argMax(metadata.deleted, inserted_at) as deleted,
        argMax(timestamp, inserted_at) as signal_ts
    FROM document_embeddings
    WHERE model_name = 'text-embedding-3-small-1536'
      AND product = 'signals'
      AND document_type = 'signal'
      AND timestamp >= now() - INTERVAL 3 MONTH
    GROUP BY document_id
)
WHERE report_id = '<report-uuid-here>'
  AND deleted != 'true'
ORDER BY signal_ts ASC
LIMIT 100

Exemple 3 : Lister les Types de Signaux

Voyez quels types de signaux existent pour ce client — retourne un exemple par paire unique (source_product, source_type) du dernier mois :

SELECT
    source_product,
    source_type,
    count() as cnt,
    max(signal_ts) as latest_timestamp
FROM (
    SELECT
        document_id,
        argMax(metadata.source_product, inserted_at) as source_product,
        argMax(metadata.source_product, inserted_at) as source_product,
        argMax(metadata.source_type, inserted_at) as source_type,
        argMax(metadata.deleted, inserted_at) as deleted,
        argMax(timestamp, inserted_at) as signal_ts
    FROM document_embeddings
    WHERE model_name = 'text-embedding-3-small-1536'
      AND product = 'signals'
      AND document_type = 'signal'
      AND timestamp >= now() - INTERVAL 1 MONTH
    GROUP BY document_id
)
WHERE deleted != 'true'
GROUP BY source_product, source_type
ORDER BY latest_timestamp DESC
LIMIT 100

Exemple 4 : Signaux Récents d'une Source Spécifique

Trouvez les derniers signaux d'une source produit particulière (par ex. tous les signaux de suivi d'erreurs) :

SELECT
    document_id,
    content,
    source_type,
    report_id,
    signal_ts
FROM (
    SELECT
        document_id,
        argMax(content, inserted_at) as content,
        argMax(metadata.source_product, inserted_at) as source_product,
        argMax(metadata.source_type, inserted_at) as source_type,
        argMax(metadata.report_id, inserted_at) as report_id,
        argMax(metadata.deleted, inserted_at) as deleted,
        argMax(timestamp, inserted_at) as signal_ts
    FROM document_embeddings
    WHERE model_name = 'text-embedding-3-small-1536'
      AND product = 'signals'
      AND document_type = 'signal'
      AND timestamp >= now() - INTERVAL 1 WEEK
    GROUP BY document_id
)
WHERE source_product = 'error_tracking'
  AND deleted != 'true'
ORDER BY signal_ts DESC
LIMIT 100

Remplacez 'error_tracking' par n'importe quelle source produit : 'web_analytics', 'experiments', 'session_replay', etc. Utilisez l'Exemple 3 pour découvrir quels source products et types existent.

Exemple 5 : Recherche Full-Text pour les Signaux

Quand vous connaissez un mot-clé ou une phrase spécifique à chercher (par ex. un nom produit, message d'erreur, ou URL), la recherche full-text avec ILIKE est plus rapide et plus précise que la recherche sémantique :

SELECT
    document_id,
    content,
    source_product,
    source_type,
    signal_ts
FROM (
    SELECT
        document_id,
        argMax(content, inserted_at) as content,
        argMax(metadata.source_product, inserted_at) as source_product,
        argMax(metadata.source_type, inserted_at) as source_type,
        argMax(metadata.deleted, inserted_at) as deleted,
        argMax(timestamp, inserted_at) as signal_ts
    FROM document_embeddings
    WHERE model_name = 'text-embedding-3-small-1536'
      AND product = 'signals'
      AND document_type = 'signal'
      AND timestamp >= now() - INTERVAL 1 MONTH
    GROUP BY document_id
)
WHERE deleted != 'true'
  AND content ILIKE '%feature flag%'
ORDER BY signal_ts DESC
LIMIT 10

Remplacez '%feature flag%' par le terme que vous cherchez. Utilisez ILIKE pour la correspondance de substring insensible à la casse. Pour la correspondance de token exacte, utilisez plutôt hasTokenCaseInsensitive(content, 'token').

Pièges

  1. Utilisez toujours text-embedding-3-small-1536 comme nom de modèle. C'est le seul modèle utilisé pour les signaux.
  2. Les arguments embedText() doivent être des littéraux string. Vous ne pouvez pas passer de références de colonnes ou d'expressions — la fonction se résout au moment de la compilation, pas par ligne.
  3. Toujours borner temporellement vos requêtes. La table a une TTL de 3 mois, mais les scans non bornés sont coûteux. Utilisez timestamp >= now() - INTERVAL 1 MONTH ou plus serré. Placez le filtre temporel dans la clause WHERE de la sous-requête interne (sur la colonne timestamp brute) pour la meilleure performance.
  4. Toujours dédupliquer. Sans la sous-requête argMax(..., inserted_at) GROUP BY document_id, vous verrez des lignes obsolètes et dupliquées.
  5. Ne sélectionnez embedding que quand vous en avez besoin. C'est un tableau de 1536 floats — omettez-le de la sous-requête interne quand vous ne faites pas de recherche par similarité.
  6. Les requêtes ne doivent pas se terminer par un point-virgule. HogQL ne les utilise pas.
  7. Ajoutez un LIMIT à chaque requête. Le maximum autorisé est 500 lignes. En général, vous ne devez sélectionner que 10 ou so signaux, en utilisant la recherche sémantique ou full text pour les classer.
  8. Extrayez les champs metadata dans la sous-requête de dédup. L'accès par point metadata.field de HogQL ne fonctionne que sur la colonne brute de table. Après agrégation argMax(), le type JSON est perdu et l'accès par point échouera avec une erreur de type. Utilisez toujours argMax(metadata.field_name, inserted_at) as field_name dans la requête interne.
  9. Toutes les valeurs d'accès JSON par point sont Nullable(String). HogQL extrait chaque champ JSON comme une String, même les booléens et nombres. Pour metadata.deleted, utilisez deleted != 'true' — n'utilisez PAS NOT deleted.
  10. N'aliasez pas argMax(timestamp, inserted_at) comme timestamp si la même requête interne filtre aussi sur la colonne timestamp brute. HogQL résout le nom d'alias en premier, causant une erreur « aggregate in WHERE ». Soit utilisez un alias distinct comme signal_ts, soit déplacez le filtre temporel vers la requête externe.

Skills similaires