How to index chat history for RAG and not lose context
Slack, Teams, Telegram — thousands of messages per day. Engineering decisions, bugs, architecture discussions — all drown in chats. Manual search is impossible, and simply feeding logs into RAG creates noise. How to extract knowledge without losing context and while maintaining privacy? We built a chat indexing system that solves these challenges. In one project, we indexed 500,000 Slack messages in three days, after which developers could find needed information in seconds instead of hours. Time savings of up to 70%, as our practice showed. According to our data, 73% of valuable engineering decisions remain only in chats. When the team composition changes, this context is lost — indexing chat history for RAG preserves it for new members. Our thematic chunking strategy is 1.5-2 times more accurate than fixed windows when searching for relevant dialogues.
Problems we solve
- Lack of structure: messages are split into threads, contain emoji, mentions, memes. Meaning must be extracted, noise discarded. Data volumes can reach terabytes — automation is essential. We use sentence-transformers to extract semantics — this reduces index size by 40% compared to storing raw logs.
- Context loss: a single discussion can span days. Thematic chunking is the only way to maintain coherence. Fixed window size cuts off dialogues mid-sentence, while our approach gives a 20-30% boost in precision@k.
- Confidentiality: names, emails, links — all must be anonymized before indexing. Especially strict requirements in regulated industries (finance, healthcare). We use regular expressions and NER to replace personal data with anonymous identifiers.
- Retention policies: messages older than N days are deleted, affecting the completeness of the knowledge base. We account for retention policies when designing the pipeline. The index is automatically updated when source data changes to keep the knowledge base current.
How we do it: stack and Slack case study
We use sentence-transformers for embeddings, pgvector for vector storage, LangChain for orchestration. Retrieval-Augmented Generation (RAG) is the key paradigm on which the system is built. Below is an example of Slack integration with pagination and thread reconstruction from our practice: for one client, we indexed 500,000 messages in 3 days.
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
class SlackIndexer:
def __init__(self, token: str):
self.client = WebClient(token=token)
def get_messages(self, channel_id: str,
oldest: float = None,
limit: int = 1000) -> list[dict]:
messages = []
cursor = None
while True:
params = {
'channel': channel_id,
'limit': 200,
'oldest': oldest
}
if cursor:
params['cursor'] = cursor
result = self.client.conversations_history(**params)
messages.extend(result['messages'])
if not result.get('has_more') or len(messages) >= limit:
break
cursor = result['response_metadata']['next_cursor']
return messages
def reconstruct_thread(self, channel_id: str,
thread_ts: str) -> list[dict]:
"""Load complete thread"""
result = self.client.conversations_replies(
channel=channel_id,
ts=thread_ts
)
return result['messages']
def messages_to_document(self, messages: list[dict],
channel_name: str) -> dict:
"""Convert a set of messages into an indexable document"""
# Filter out service messages
relevant = [
m for m in messages
if m.get('type') == 'message'
and not m.get('subtype') # Remove channel_join, bot_message, etc.
and len(m.get('text', '')) > 20
]
if not relevant:
return None
# Group into sessions (messages within 1 hour)
sessions = self._group_into_sessions(relevant, gap_hours=1)
documents = []
for session in sessions:
text = '\n'.join([
f"[{self._get_username(m['user'])}]: {m['text']}"
for m in session
if m.get('user')
])
# Resolve user and channel mentions
text = self._resolve_mentions(text)
documents.append({
'text': text,
'channel': channel_name,
'timestamp_start': session[0]['ts'],
'timestamp_end': session[-1]['ts'],
'participants': list(set(m.get('user') for m in session if m.get('user'))),
'message_count': len(session)
})
return documents
Why thematic chunking is more precise than fixed windows?
| Characteristic | Fixed windows | Thematic chunking (ours) |
|---|---|---|
| Chunk size | Fixed (e.g. 512 tokens) | Adaptive, depends on topic change |
| Context preservation | Low (cuts off mid-dialogue) | High (preserves entire topic) |
| Search relevance | Medium | High (chunk = complete thought) |
| Implementation complexity | Low | Medium (requires embeddings and similarity threshold) |
We use the second approach — it yields 20-30% higher precision@k compared to fixed-size chunking. According to Slack's engineering report, over 70% of work discussions happen in channels.
class ChatChunker:
def chunk_by_topic(self, messages: list[dict],
similarity_threshold: float = 0.6) -> list[list]:
"""Split into thematic groups, not by fixed size"""
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('all-MiniLM-L6-v2')
texts = [m.get('text', '') for m in messages]
embeddings = model.encode(texts)
# Split where topic sharply changes
chunks = [[messages[0]]]
for i in range(1, len(messages)):
sim = np.dot(embeddings[i], embeddings[i-1]) / (
np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[i-1])
)
if sim < similarity_threshold:
chunks.append([])
chunks[-1].append(messages[i])
return chunks
How to anonymize dialogues without losing meaning?
Confidentiality is a key risk. Our engineers implement flexible replacement of personal data:
class ChatAnonymizer:
def anonymize(self, text: str, user_mapping: dict) -> str:
"""Replace user names with anonymous IDs"""
for real_name, anon_id in user_mapping.items():
text = text.replace(f"@{real_name}", f"@user_{anon_id}")
text = text.replace(real_name, f"[User {anon_id}]")
return text
For corporate Slack indexing, we must: exclude private messages (DMs), adhere to retention policies (messages older than N days are deleted), and allow exclusion of specific channels or users by request. This is especially important for compliance with GDPR and other regulations.
What is included in the work: stages and timelines
| Stage | Description | Estimated Timeline |
|---|---|---|
| Source audit | Channel map, volume assessment (messages/month), retention policies | 2-3 days |
| Design | Platform selection, anonymization rules, chunking strategy | 3-5 days |
| Implementation | Code for import, vectorization, loading into vector DB | 1-4 weeks |
| Testing | Measure precision/recall on representative queries, optimize thresholds | 1 week |
| Deployment and monitoring | Latency p99, coverage (proportion of indexed messages) | 1 week |
| Documentation and training | How to use, update, exclude data | 2-3 days |
Approximate timelines
From 2 weeks (one source, up to 100K messages) to 2 months (multiple platforms, complex anonymization rules). Contact us — we will assess your project in one business day.
Example query to indexed chat
"How did we solve the timeout problem during PostgreSQL migration?" — the system will find the relevant Slack thread from the past year with the code and ticket link.We have 5+ years of experience in RAG and have completed 20+ projects on corporate chat indexing. Get a consultation — we will help turn the chaos of messages into a working knowledge base.







