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 surdocument_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
- Utilisez toujours
text-embedding-3-small-1536comme nom de modèle. C'est le seul modèle utilisé pour les signaux. - 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. - 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 MONTHou plus serré. Placez le filtre temporel dans la clauseWHEREde la sous-requête interne (sur la colonnetimestampbrute) pour la meilleure performance. - Toujours dédupliquer. Sans la sous-requête
argMax(..., inserted_at) GROUP BY document_id, vous verrez des lignes obsolètes et dupliquées. - Ne sélectionnez
embeddingque 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é. - Les requêtes ne doivent pas se terminer par un point-virgule. HogQL ne les utilise pas.
- 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. - Extrayez les champs metadata dans la sous-requête de dédup. L'accès par point
metadata.fieldde HogQL ne fonctionne que sur la colonne brute de table. Après agrégationargMax(), le type JSON est perdu et l'accès par point échouera avec une erreur de type. Utilisez toujoursargMax(metadata.field_name, inserted_at) as field_namedans la requête interne. - 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. Pourmetadata.deleted, utilisezdeleted != 'true'— n'utilisez PASNOT deleted. - N'aliasez pas
argMax(timestamp, inserted_at)commetimestampsi la même requête interne filtre aussi sur la colonnetimestampbrute. HogQL résout le nom d'alias en premier, causant une erreur « aggregate in WHERE ». Soit utilisez un alias distinct commesignal_ts, soit déplacez le filtre temporel vers la requête externe.