Notion + RAG : Connecter votre wiki d'entreprise
Guide complet pour integrer Notion comme source de connaissances pour un chatbot RAG. Synchronisation, indexation, recherche semantique et cas d'usage pratiques.
Notion + RAG : Connecter votre wiki d'entreprise
Notion est devenu le wiki de reference pour des milliers d'entreprises. Sa flexibilite, son interface intuitive et ses fonctionnalites de collaboration en font un outil incontournable pour centraliser la connaissance d'equipe. Mais a mesure que votre workspace grandit, un probleme emerge : retrouver l'information devient un cauchemar. Avec des centaines de pages, sous-pages et bases de donnees, meme les utilisateurs les plus experimentes passent de precieuses minutes a chercher ce qu'ils savent exister quelque part.
Un chatbot RAG connecte a Notion transforme cette masse documentaire en assistant intelligent. Au lieu de naviguer, vous posez une question en langage naturel et obtenez une reponse synthetisee, sourcee et contextualisee. Ce guide vous montre comment realiser cette integration pas a pas.
Pourquoi connecter Notion au RAG ?
Les limites de la recherche Notion native
La recherche integree de Notion, bien qu'utile, presente des limitations significatives pour les grandes organisations :
| Probleme | Impact concret |
|---|---|
| Recherche par mots-cles uniquement | "Comment demander des conges" ne trouve pas "procedure d'absence" |
| Pas de recherche dans les bases de donnees | Les proprietes et champs ne sont pas indexes |
| Resultats non classes par pertinence | Pages recentes privilegiees sur les plus pertinentes |
| Pas de synthese | L'utilisateur doit ouvrir et lire chaque page |
| Pas de contexte conversationnel | Chaque recherche repart de zero |
Ce qu'apporte le RAG
L'approche RAG (Retrieval-Augmented Generation) resout ces limitations en combinant recherche semantique et generation de langage :
- Recherche semantique : Trouve l'information meme formulee differemment. "Comment poser des RTT" matchera "Procedure de demande de jours de repos"
- Synthese intelligente : Repond directement sans obliger a naviguer dans 5 pages
- Agregation multi-pages : Combine les informations de plusieurs sources pour une reponse complete
- Memoire conversationnelle : Chaque question beneficie du contexte des echanges precedents
- Citations sourcees : Chaque affirmation renvoie vers la page d'origine
Architecture Notion + RAG
L'integration suit une architecture en trois couches qui separe l'extraction, l'indexation et l'interrogation :
┌─────────────────────────────────────────────────────────────────────────┐
│ Architecture Notion + RAG │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ EXTRACTION INDEXATION INTERROGATION │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Notion API │───────────▶│ Chunking │─────────▶│ Qdrant │ │
│ │ │ │ │ │ │ │
│ │ - Pages │ │ - Sections │ │ Vecteurs │ │
│ │ - DBs │ │ - 500 tokens│ │ │ │
│ │ - Blocs │ │ - Overlap │ └──────┬─────┘ │
│ └──────────────┘ └──────────────┘ │ │
│ │ │ │
│ ┌──────┴──────┐ │ │
│ │ Embeddings │ │ │
│ │ BGE-M3 │ │ │
│ └─────────────┘ │ │
│ │ │
│ CHATBOT │ │
│ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ │ │
│ │ Question │────▶│ Retrieval │◀────│ Reranker │◀──┘ │
│ │ utilisateur │ │ Top-20 │ │ Top-5 │ │
│ └──────────────┘ └─────────────┘ └──────┬───────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ LLM │ │
│ │ Reponse │ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Composants cles
- Extraction : Le connecteur Notion utilise l'API officielle pour recuperer pages et bases de donnees
- Chunking : Les documents longs sont decoupes en segments de 500 tokens avec chevauchement
- Embeddings : Chaque chunk est transforme en vecteur semantique (BGE-M3 pour le multilingue)
- Base vectorielle : Qdrant stocke et indexe les vecteurs pour recherche rapide
- Reranking : Un second modele reordonne les resultats par pertinence
- Generation : Le LLM synthetise une reponse a partir des chunks pertinents
Connecteur Notion complet
Voici une implementation de reference pour extraire le contenu Notion :
DEVELOPERpythonfrom notion_client import Client from datetime import datetime import hashlib class NotionConnector: def __init__(self, token: str): """Initialise le connecteur avec le token d'integration.""" self.client = Client(auth=token) self.processed_ids = set() def get_all_pages(self, filter_by_parent: str = None) -> list: """ Recupere toutes les pages accessibles par l'integration. Args: filter_by_parent: ID de page parent pour filtrer (optionnel) Returns: Liste de documents formates pour le RAG """ pages = [] has_more = True cursor = None while has_more: results = self.client.search( filter={"property": "object", "value": "page"}, start_cursor=cursor, page_size=100 ) for page in results['results']: # Eviter les doublons if page['id'] in self.processed_ids: continue # Filtrer par parent si specifie if filter_by_parent: parent = page.get('parent', {}) if parent.get('page_id') != filter_by_parent: continue doc = self._format_page(page) if doc and len(doc['content']) > 50: # Ignorer pages vides pages.append(doc) self.processed_ids.add(page['id']) has_more = results['has_more'] cursor = results.get('next_cursor') return pages def _format_page(self, page: dict) -> dict: """Formate une page Notion en document RAG.""" title = self._extract_title(page) content = self._extract_content(page['id']) # Generer un hash pour detecter les changements content_hash = hashlib.md5(content.encode()).hexdigest() return { "id": f"notion_{page['id']}", "title": title, "content": f"# {title}\n\n{content}", "metadata": { "source": "notion", "source_type": "wiki", "page_id": page['id'], "url": page.get('url', ''), "last_edited": page['last_edited_time'], "created_time": page['created_time'], "content_hash": content_hash, "parent_type": page.get('parent', {}).get('type'), "icon": self._extract_icon(page) } } def _extract_title(self, page: dict) -> str: """Extrait le titre d'une page.""" props = page.get('properties', {}) # Chercher dans les proprietes 'title' ou 'Name' for key in ['title', 'Title', 'Name', 'name']: if key in props and props[key].get('title'): title_parts = props[key]['title'] return ''.join([t['plain_text'] for t in title_parts]) return "Sans titre" def _extract_content(self, page_id: str) -> str: """Extrait le contenu textuel complet d'une page.""" content_parts = [] def process_blocks(block_id: str, depth: int = 0): """Recursif pour gerer les blocs imbriques.""" if depth > 5: # Limite de profondeur return blocks = self.client.blocks.children.list(block_id=block_id) for block in blocks['results']: text = self._block_to_text(block, depth) if text: content_parts.append(text) # Traiter les enfants si le bloc en a if block.get('has_children'): process_blocks(block['id'], depth + 1) process_blocks(page_id) return "\n\n".join(content_parts) def _block_to_text(self, block: dict, depth: int = 0) -> str: """Convertit un bloc Notion en Markdown.""" block_type = block['type'] indent = " " * depth handlers = { 'paragraph': lambda b: self._rich_text(b['paragraph']['rich_text']), 'heading_1': lambda b: f"# {self._rich_text(b['heading_1']['rich_text'])}", 'heading_2': lambda b: f"## {self._rich_text(b['heading_2']['rich_text'])}", 'heading_3': lambda b: f"### {self._rich_text(b['heading_3']['rich_text'])}", 'bulleted_list_item': lambda b: f"{indent}- {self._rich_text(b['bulleted_list_item']['rich_text'])}", 'numbered_list_item': lambda b: f"{indent}1. {self._rich_text(b['numbered_list_item']['rich_text'])}", 'to_do': lambda b: f"{indent}- [{'x' if b['to_do']['checked'] else ' '}] {self._rich_text(b['to_do']['rich_text'])}", 'toggle': lambda b: f"{indent}> {self._rich_text(b['toggle']['rich_text'])}", 'quote': lambda b: f"> {self._rich_text(b['quote']['rich_text'])}", 'callout': lambda b: f"> {b['callout'].get('icon', {}).get('emoji', '')} {self._rich_text(b['callout']['rich_text'])}", 'code': lambda b: f"```{b['code']['language']}\n{self._rich_text(b['code']['rich_text'])}\n```", 'divider': lambda b: "---", 'table_row': lambda b: self._table_row_to_text(b), } handler = handlers.get(block_type) return handler(block) if handler else "" def _rich_text(self, rich_text: list) -> str: """Convertit le rich text Notion en texte avec mise en forme Markdown.""" parts = [] for rt in rich_text: text = rt['plain_text'] annotations = rt.get('annotations', {}) if annotations.get('bold'): text = f"**{text}**" if annotations.get('italic'): text = f"*{text}*" if annotations.get('code'): text = f"`{text}`" if rt.get('href'): text = f"[{text}]({rt['href']})" parts.append(text) return ''.join(parts) def _table_row_to_text(self, block: dict) -> str: """Convertit une ligne de tableau.""" cells = block['table_row']['cells'] row = [self._rich_text(cell) for cell in cells] return "| " + " | ".join(row) + " |" def _extract_icon(self, page: dict) -> str: """Extrait l'icone de la page.""" icon = page.get('icon', {}) if icon.get('type') == 'emoji': return icon.get('emoji', '') return '' class NotionDatabaseConnector(NotionConnector): """Extension pour extraire les bases de donnees Notion.""" def get_database_entries(self, database_id: str) -> list: """ Recupere toutes les entrees d'une base de donnees. Chaque entree devient un document avec ses proprietes comme metadonnees structurees. """ entries = [] has_more = True cursor = None while has_more: results = self.client.databases.query( database_id=database_id, start_cursor=cursor, page_size=100 ) for entry in results['results']: doc = self._format_database_entry(entry, database_id) if doc: entries.append(doc) has_more = results['has_more'] cursor = results.get('next_cursor') return entries def _format_database_entry(self, entry: dict, db_id: str) -> dict: """Formate une entree de base de donnees.""" props = entry.get('properties', {}) # Extraire toutes les proprietes comme texte structure prop_texts = [] metadata_props = {} for name, prop in props.items(): value = self._extract_property_value(prop) if value: prop_texts.append(f"**{name}**: {value}") metadata_props[name] = value title = metadata_props.get('Name', metadata_props.get('Titre', 'Entree')) content = "\n".join(prop_texts) # Ajouter le contenu de la page si elle en a page_content = self._extract_content(entry['id']) if page_content: content += f"\n\n{page_content}" return { "id": f"notion_db_{entry['id']}", "title": title, "content": f"# {title}\n\n{content}", "metadata": { "source": "notion", "source_type": "database", "database_id": db_id, "entry_id": entry['id'], "url": entry.get('url', ''), "last_edited": entry['last_edited_time'], **metadata_props } } def _extract_property_value(self, prop: dict) -> str: """Extrait la valeur d'une propriete Notion.""" prop_type = prop.get('type') extractors = { 'title': lambda p: self._rich_text(p.get('title', [])), 'rich_text': lambda p: self._rich_text(p.get('rich_text', [])), 'number': lambda p: str(p.get('number', '')), 'select': lambda p: p.get('select', {}).get('name', '') if p.get('select') else '', 'multi_select': lambda p: ', '.join([s['name'] for s in p.get('multi_select', [])]), 'date': lambda p: p.get('date', {}).get('start', '') if p.get('date') else '', 'checkbox': lambda p: 'Oui' if p.get('checkbox') else 'Non', 'url': lambda p: p.get('url', ''), 'email': lambda p: p.get('email', ''), 'phone_number': lambda p: p.get('phone_number', ''), 'status': lambda p: p.get('status', {}).get('name', '') if p.get('status') else '', } extractor = extractors.get(prop_type) return extractor(prop) if extractor else ''
Synchronisation intelligente
La synchronisation peut etre declenchee de plusieurs manieres selon vos besoins :
Synchronisation par polling
DEVELOPERpythonfrom datetime import datetime, timedelta class NotionSyncManager: def __init__(self, connector: NotionConnector, indexer): self.connector = connector self.indexer = indexer self.last_sync = None def sync_incremental(self): """ Synchronisation incrementale : ne traite que les pages modifiees depuis la derniere synchronisation. """ pages = self.connector.get_all_pages() updated = [] for page in pages: last_edited = datetime.fromisoformat( page['metadata']['last_edited'].replace('Z', '+00:00') ) if self.last_sync is None or last_edited > self.last_sync: updated.append(page) if updated: self.indexer.upsert_documents(updated) print(f"Synchronise {len(updated)} pages") self.last_sync = datetime.now() def sync_full(self): """Synchronisation complete : re-indexe tout.""" pages = self.connector.get_all_pages() self.indexer.replace_all(pages) self.last_sync = datetime.now() print(f"Indexe {len(pages)} pages")
Synchronisation temps reel
Pour une synchronisation en temps reel, utilisez les webhooks Notion (disponibles via l'API) ou un worker qui poll regulierement avec une granularite fine :
DEVELOPERpythonimport schedule import time def start_sync_worker(sync_manager: NotionSyncManager): """Demarre le worker de synchronisation.""" # Synchronisation incrementale toutes les 5 minutes schedule.every(5).minutes.do(sync_manager.sync_incremental) # Synchronisation complete quotidienne (nettoyage) schedule.every().day.at("03:00").do(sync_manager.sync_full) while True: schedule.run_pending() time.sleep(60)
Prompt systeme optimise pour Notion
Le prompt systeme est crucial pour obtenir des reponses de qualite. Voici une version optimisee pour les wikis d'entreprise :
DEVELOPERpythonNOTION_KB_SYSTEM_PROMPT = """Tu es l'assistant base de connaissances de {company_name}. Tu aides les employes a trouver rapidement les informations dans notre wiki Notion. ## Ta mission - Repondre aux questions en te basant UNIQUEMENT sur le contenu du wiki - Citer systematiquement les sources avec le titre de la page - Orienter vers la bonne personne/equipe si la question depasse le wiki ## Regles strictes 1. Ne jamais inventer d'information absente du wiki 2. Si tu n'es pas sur, dis-le clairement 3. Privilegier les informations les plus recentes 4. Adapter le niveau de detail a la question (resume vs detail) ## Format de reponse - Commence par repondre directement a la question - Ajoute des details si pertinents - Termine par "Source: [Titre de la page]" pour chaque information ## Contexte disponible {context} ## Historique de conversation {chat_history} """ def build_notion_prompt(company: str, docs: list, history: list) -> str: """Construit le prompt avec le contexte.""" context_parts = [] for doc in docs: context_parts.append(f"### {doc['title']}\n{doc['content'][:1000]}") history_parts = [] for msg in history[-5:]: # 5 derniers messages role = "Utilisateur" if msg['role'] == 'user' else "Assistant" history_parts.append(f"{role}: {msg['content']}") return NOTION_KB_SYSTEM_PROMPT.format( company_name=company, context="\n\n---\n\n".join(context_parts), chat_history="\n".join(history_parts) if history_parts else "Aucun" )
Cas d'usage concrets
1. Onboarding des nouveaux employes
Question : "Comment configurer mon environnement de developpement ?"
Reponse RAG :
Pour configurer ton environnement de dev, voici les etapes :
- Clone le repo principal :
git clone [email protected]:company/main.git- Installe les dependances :
npm install- Copie le fichier
.env.examplevers.env- Demande les credentials a l'equipe DevOps sur Slack (#devops-support)
Source: [Guide Setup Dev] | [Onboarding Tech]
2. Questions RH frequentes
Question : "Comment poser des conges ?"
Reponse RAG :
Pour poser des conges :
- Va sur Payfit > Absences > Nouvelle demande
- Selectionne le type (CP, RTT, sans solde)
- Indique les dates et valide
Delai de validation : 48h par ton manager. Pour les urgences, contacte RH.
Source: [Procedure conges et absences]
3. Documentation technique
Question : "C'est quoi l'architecture du service de paiement ?"
Reponse RAG :
Le service de paiement suit une architecture event-driven :
- API Gateway : Authentification et routing
- Payment Service : Orchestration des transactions
- Stripe Adapter : Integration Stripe
- Event Bus : Kafka pour les notifications
Diagramme complet disponible sur la page dediee.
Source: [Architecture Payment Service] | [Diagrammes techniques]
Bonnes pratiques
Structurer Notion pour le RAG
| Pratique | Pourquoi |
|---|---|
| Titres descriptifs | Ameliore la recherche |
| Structure hierarchique claire | Facilite le chunking |
| Mise a jour des dates | Permet de prioriser le recent |
| Tags et categories | Enrichit les metadonnees |
| Liens internes | Aide le contexte |
Gerer les permissions
Le RAG herite des permissions de l'integration Notion. Pour un controle granulaire :
- Creer une integration dediee au RAG
- Partager uniquement les pages publiques avec l'integration
- Gerer les acces par workspace si multi-tenant
Monitorer la qualite
- Tracker les questions sans reponse
- Collecter les feedbacks utilisateurs
- Identifier les pages les plus citees
- Detecter le contenu obsolete
Ressources complementaires
- Base de connaissances entreprise - Guide pilier complet
- Confluence + RAG - Pour les environnements Atlassian
- SharePoint + RAG - Pour Microsoft 365
- Introduction au RAG - Les fondamentaux
FAQ
Connectez Notion avec Ailog
Transformez votre wiki Notion en assistant intelligent sans ecrire une ligne de code. Ailog simplifie l'integration :
- Connecteur Notion natif : Synchronisation automatique en quelques clics
- Recherche semantique : Trouvez l'info avec vos mots, pas ceux du wiki
- Multi-workspace : Gerez plusieurs espaces Notion dans une meme interface
- Controle des acces : Respectez les permissions de votre organisation
- Hebergement France : Donnees sur serveurs francais, conformite RGPD native
Testez Ailog gratuitement et deployez votre assistant Notion en 10 minutes.
Tags
Articles connexes
Confluence : Base de connaissances IA pour equipes
Guide complet pour deployer un assistant RAG sur Confluence. Transformez votre documentation Atlassian en base de connaissances interrogeable par IA.
Slack Bot RAG : Recherche intelligente dans vos conversations
Guide complet pour deployer un bot Slack RAG. Transformez l'historique de vos canaux en base de connaissances interrogeable par IA.
SharePoint + RAG : Exploiter vos documents Microsoft 365
Guide complet pour connecter SharePoint a un systeme RAG. Rendez vos documents Microsoft 365 interrogeables par IA avec recherche semantique.