Rechercher des contacts
search_personssearch_persons retrouve des contacts : plein texte déterministe sur tous les champs descriptifs de la fiche — nom, notes, email, téléphone, adresse — combiné à des filtres sur le graphe relationnel : restriction par kind, et par le rôle porté par un contact lié (with_role / without_role). Il ne crée jamais rien.
Comment le matching fonctionne
La requête s'exécute en recherche plein texte PostgreSQL sur les champs descriptifs indexés du contact. Le matching est insensible aux accents (« francois » trouve « François »), par mots entiers avec racinisation, pas par sous-chaîne. Les filtres de rôle s'exécutent dans la même fonction base de données, restreinte par visibilité, que le matching texte.
- L'index plein texte couvre exactement les champs descriptifs : nom, notes, email, téléphone et adresse.
- Les numéros d'immatriculation et de TVA ne sont pas dans l'index plein texte : lisez-les sur la fiche elle-même (get_person_context) plutôt que de les chercher via query.
- Insensible aux accents dans les deux sens : une requête sans accents matche le texte stocké accentué, et une requête accentuée matche le texte sans accents.
- Un rôle matche le label d'une relation (lu dans les deux sens) OU un tag porté par le contact lié : with_role=fournisseur trouve les contacts reliés à quelqu'un étiqueté fournisseur ou tagué fournisseur.
- without_role est un anti-join exécuté côté serveur : il ne garde que les contacts sans AUCUN lien correspondant au rôle, évalué sur toute la base de contacts — pas un filtre côté client sur une première page de résultats.
- La visibilité s'applique aux contacts renvoyés ET aux deux extrémités de chaque relation considérée : un lien vers un contact que vous ne pouvez pas voir ne matche aucun rôle et n'apparaît pas dans links.
- without_tags prend des slugs de tags séparés par des virgules et fonctionne comme without_role : un anti-join côté serveur qui ne garde que les contacts ne portant AUCUN d'entre eux, évalué sur toute la base de contacts — pas un filtre côté client sur une première page de résultats.
- Avec une recherche, les noms exacts passent en premier, puis les correspondances dans le nom, puis celles dans les notes et les autres champs. Chaque groupe est trié par nom puis par identifiant. Sans recherche, le tri est alphabétique. limit vaut 20 par défaut et accepte 1-50 par page (entiers uniquement) ; les valeurs invalides sont rejetées.
- Repassez next_cursor en cursor avec les mêmes filtres pour obtenir la page suivante (limit peut changer). Il vaut null sur la dernière page. Les curseurs de recherche sont liés aux filtres et à l'utilisateur. Les anciens curseurs alphabétiques conservent leur ordre. Modifier un contact peut changer son rang ou son nom et le déplacer entre les pages.
Paramètres
Tout ce que l'outil accepte, directement depuis l'inputSchema en vigueur. Tous les paramètres sont facultatifs ; une clé absente du schéma est rejetée, jamais ignorée.
| Paramètre | Type | Description |
|---|---|---|
| query | string (<= 200) | Recherche plein texte sur le nom, les notes, l'email, le téléphone et l'adresse du contact. Insensible aux accents, par mots entiers avec racinisation, pas de sous-chaîne. |
| kind | "individual" | "company" | Restreint aux personnes physiques (individual) ou aux entreprises (company). |
| with_role | string (<= 100) | Uniquement les contacts ayant un contact lié dans ce rôle — matché sur le label de la relation (dans les deux sens) ou sur un tag du contact lié. |
| without_role | string (<= 100) | Uniquement les contacts sans AUCUN contact lié dans ce rôle. Exemple : kind=company avec without_role=commercial liste les entreprises sans commercial. |
| tags | string (<= 200) | Slugs d'inclusion séparés par des virgules. Correspond à AU MOINS UN tag par défaut, ou à TOUS avec match_all_tags=true. Se combine avec without_tags, la recherche, le type et les rôles avant pagination. Chaque entrée doit être un slug valide : une entrée inutilisable (ou un texte vide) est une erreur, jamais ignorée en silence -- en mode TOUS, une entrée ignorée réduirait la demande. Un slug présent à la fois dans tags et without_tags est une erreur quand rien ne peut correspondre : en mode TOUS, ou quand tous les slugs inclus sont exclus (un chevauchement partiel en mode AU MOINS UN ne contribue simplement pas). Omettez tags pour ne pas filtrer par inclusion. |
| match_all_tags | boolean | Si vrai, exige chaque tag fourni dans tags plutôt qu'un seul d'entre eux. Ignoré si tags est omis. Un tag inconnu rend le résultat vide en mode TOUS. |
| without_tags | string (<= 200) | Slugs de tags séparés par des virgules. Ne garde que les contacts ne portant AUCUN d'entre eux — un anti-join côté serveur sur toute la base de contacts, comme without_role. Chaque entrée doit être un slug valide : une entrée inutilisable est une erreur, jamais ignorée (l'ignorer laisserait passer les contacts qu'elle nommait), et une valeur ne contenant aucun slug valide est une erreur, jamais un résultat non filtré. Exemple : without_tags="client,fournisseur,prospect" pour les contacts que personne n'a encore qualifiés. |
| limit | integer (1-50, default 20) | Nombre maximal de résultats par page. 20 par défaut ; une valeur hors plage ou non entière est rejetée. |
| cursor | string | Curseur de pagination opaque issu du next_cursor d'une réponse précédente. Omettez-le pour la première page. |
Réponse
Chaque contact revient sous forme compacte : identité, téléphone et email, un aperçu des notes, les tags, et ses liens relationnels avec leurs labels orientés. Récupérez la fiche complète avec get_person_context.
{
"contacts": [
{
"id": "b2ce0d4f-7e19-4c2b-8f3a-5d6e7f8a9b0c",
"display_name": "François Martin",
"kind": "individual",
"phone": "+33 6 12 34 56 78",
"email": "francois@boulangerie-martin.fr",
"notes": "Prefers morning deliveries - ring the back door...",
"tags": [
"supplier"
],
"links": [
{
"id": "0a1b2c3d-4e5f-4789-8bcd-ef0123456789",
"name": "Boulangerie Martin",
"kind": "company",
"relationship": "manager",
"note": null
}
]
}
],
"next_cursor": null
}{
"contacts": [],
"next_cursor": null
}{
"type": "object",
"properties": {
"contacts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"display_name": {
"type": "string"
},
"kind": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"phone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"email": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"notes": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"links": {
"type": "array",
"items": {
"type": "object",
"properties": {
"relationship": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"note": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": {}
}
}
},
"required": [
"id",
"display_name",
"kind",
"phone",
"email",
"notes",
"tags",
"links"
],
"additionalProperties": false
}
},
"next_cursor": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"contacts",
"next_cursor"
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false
}Exemples d'utilisation
Retrouver un contact par un détail descriptif
{
"query": "morning deliveries"
}{
"contacts": [
{
"id": "b2ce0d4f-...",
"display_name": "François Martin",
"kind": "individual",
"notes": "Prefers morning deliveries - ring the back door...",
"tags": [
"supplier"
]
}
],
"next_cursor": null
}Chercher malgré un accent manquant
{
"query": "francois"
}{
"contacts": [
{
"id": "b2ce0d4f-...",
"display_name": "François Martin",
"kind": "individual",
"email": "francois@boulangerie-martin.fr",
"tags": [
"supplier"
]
}
],
"next_cursor": null
}Les entreprises ayant un contact dans un rôle
{
"kind": "company",
"with_role": "sales"
}{
"contacts": [
{
"id": "7d8e9f0a-...",
"display_name": "Atelier Lemaire",
"kind": "company",
"tags": [
"client"
],
"links": [
{
"id": "3c4d5e6f-...",
"name": "Claire Lemaire",
"kind": "individual",
"relationship": "sales",
"note": null
}
]
}
],
"next_cursor": null
}Les entreprises sans un rôle
{
"kind": "company",
"without_role": "sales"
}{
"contacts": [
{
"id": "9e0f1a2b-...",
"display_name": "Garage Petit",
"kind": "company",
"tags": [
"prospect"
],
"links": []
}
],
"next_cursor": null
}Un rôle peut venir d'un label ou d'un tag
{
"with_role": "supplier"
}{
"contacts": [
{
"id": "b2ce0d4f-...",
"display_name": "François Martin",
"kind": "individual",
"tags": [],
"links": [
{
"id": "0a1b2c3d-...",
"name": "Boulangerie Martin",
"kind": "company",
"relationship": "supplier",
"note": null
}
]
},
{
"id": "5f6a7b8c-...",
"display_name": "Vergers Rousseau",
"kind": "company",
"tags": [
"supplier"
],
"links": []
}
],
"next_cursor": null
}Contacts non qualifiés, page par page
{
"without_tags": "client,fournisseur,prospect",
"limit": 2
}{
"contacts": [
{
"id": "8e9f0a1b-...",
"display_name": "Cave Dupuis",
"kind": "company",
"tags": [],
"links": []
},
{
"id": "4a5b6c7d-...",
"display_name": "Fleuriste Bernard",
"kind": "company",
"tags": [
"newsletter"
],
"links": []
}
],
"next_cursor": "NGE1YjZjN2QtLi4ufEZsZXVy..."
}Garanties et limites
- Les deux recherches sont des recherches plein texte PostgreSQL déterministes : même requête, mêmes données, mêmes résultats. Rien de sémantique, pas de vecteurs — la requête matche des mots, pas des sens.
- La visibilité est résolue avant le matching : le serveur restreint d'abord la requête aux enregistrements que l'appelant a le droit de voir, puis seulement cherche des correspondances.
- Un enregistrement invisible est indistinguable d'un enregistrement inexistant : la recherche ne confirme jamais l'existence de données masquées.
- Les index sont recalculés dans la même transaction que chaque écriture : une entrée est cherchable à l'instant où elle est créée, sans indexeur en arrière-plan ni fenêtre d'incohérence.
- Historis n'exécute aucun modèle d'IA pour retrouver les résultats. Votre agent décide quoi chercher puis classe et interprète ce qui revient ; le matching lui-même est une simple recherche en base.
Deux recherches, deux métiers
- search_events
- Cherche dans la timeline : ce qui s'est passé, ce qui est dû, ce qui a changé. Compose le plein texte avec des filtres métier et temporels, paginé par curseur keyset.
- search_persons
- Cherche dans les fiches contact et le graphe relationnel : qui est quelqu'un, comment il est relié, quel rôle porte le lien.