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.
- Les résultats sont triés par nom d'affichage, avec l'id du contact comme départage stable ; limit vaut 20 par défaut et accepte 1-50 par page (entiers uniquement) — une valeur hors plage ou non entière est rejetée avec une erreur de validation, jamais plafonnée silencieusement.
- next_cursor est un jeton keyset opaque : repassez-le en cursor pour obtenir la page suivante. Il vaut null sur la dernière page, si bien qu'une page courte n'est jamais une troncature silencieuse. Contrairement à une pagination OFFSET, les lignes insérées ou supprimées entre deux pages ne décalent jamais les frontières de page ; un contact renommé peut en revanche changer de page.
Paramètres
Tout ce que l'outil accepte, directement depuis l'inputSchema en vigueur. Tous les paramètres sont facultatifs.
| 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. |
| 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. 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": "http://json-schema.org/draft-07/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.