Picture this: a call center operator handling a customer request has to switch between Confluence, Jira, SharePoint, and Outlook to gather information. In an enterprise context, data is scattered across dozens of systems—Confluence for documentation, Jira for tasks, SharePoint for files, corporate email, CRM, external databases—each with its own search interface. Employees spend up to 40% of their time searching and switching between systems. Federated search with a single search bar and unified ranking cuts that time by 2-3x. We design and implement such systems turnkey: from requirements analysis to deployment on client infrastructure. Our experience: 5+ years in AI/ML and Enterprise AI, 15+ federated search projects delivered. Typical pilot cost ranges from $15,000 to $25,000, fully recouped within 6 months through productivity gains. We guarantee SLA on latency (p99 < 1 sec) and result quality (NDCG@10 > 0.85).
Why Federated Search Is Harder Than Regular Search
The reason is source heterogeneity. BM25 scores from Elasticsearch are incomparable with vector proximity from Qdrant. Response times vary: Confluence may respond in 100 ms, while an external API might take 2 seconds. An asynchronous architecture with timeouts, score normalization, deduplication, and ranking is required. These are non-trivial MLOps challenges. Our multi-source search solution integrates seamlessly with your existing systems, and our result ranking model ensures the most relevant documents appear first.
How Hybrid Architecture Works
In most enterprise projects, we use a hybrid approach: Confluence, SharePoint, Jira—through a centralized index; external APIs and real-time data—through query-time federation.
| Approach | Latency | Data Freshness | Complexity | Suitable For |
|---|---|---|---|---|
| Centralized index | 100–500 ms | ~15–60 min delay | High (ETL) | Large volumes, enterprise search |
| Query-time federation | 1–5 sec | Real-time | Medium | API sources, small volumes |
| Hybrid | 300–800 ms | Critical—real-time | High | Enterprise with mixed requirements |
Hybrid architecture achieves latency of 300-800 ms, which is 2-5x faster than pure query-time federation.
import asyncio
from typing import Protocol, runtime_checkable
from dataclasses import dataclass
@dataclass
class SearchResult:
source: str
doc_id: str
title: str
snippet: str
url: str
score: float
metadata: dict
@runtime_checkable
class SearchConnector(Protocol):
async def search(self, query: str, filters: dict, limit: int) -> list[SearchResult]:
...
class FederatedSearchOrchestrator:
def __init__(self, connectors: dict[str, SearchConnector]):
self.connectors = connectors
self.merger = ResultMerger()
async def search(
self,
query: str,
sources: list[str] = None,
filters: dict = None,
limit: int = 10
) -> list[SearchResult]:
active_connectors = {
name: conn for name, conn in self.connectors.items()
if sources is None or name in sources
}
# Parallel query to all sources with timeout
tasks = {
name: asyncio.create_task(
asyncio.wait_for(
conn.search(query, filters or {}, limit * 2),
timeout=3.0 # don't wait for slow sources longer than 3 sec
)
)
for name, conn in active_connectors.items()
}
results_by_source = {}
for name, task in tasks.items():
try:
results_by_source[name] = await task
except asyncio.TimeoutError:
results_by_source[name] = [] # source did not respond
except Exception as e:
results_by_source[name] = []
return self.merger.merge_and_rank(results_by_source, limit)
How Deduplication and Re-ranking Are Done
The main technical challenge in federated search is normalizing scores from different sources. BM25 scores from Elasticsearch are incomparable with cosine similarity from Qdrant. We use a CrossEncoder for final ranking and cosine similarity of embeddings for deduplication. More about CrossEncoder can be found in the documentation.
from sentence_transformers import CrossEncoder
import numpy as np
class ResultMerger:
def __init__(self):
self.reranker = CrossEncoder(
"cross-encoder/ms-marco-MiniLM-L-6-v2",
max_length=512
)
def merge_and_rank(
self,
results_by_source: dict[str, list[SearchResult]],
limit: int
) -> list[SearchResult]:
all_results = []
for source, results in results_by_source.items():
all_results.extend(results)
if not all_results:
return []
# Deduplication by content (cosine similarity of embeddings)
all_results = self._deduplicate(all_results, threshold=0.92)
# Normalize scores within each source (min-max)
for source in results_by_source:
source_results = [r for r in all_results if r.source == source]
if len(source_results) > 1:
scores = [r.score for r in source_results]
min_s, max_s = min(scores), max(scores)
for r in source_results:
r.score = (r.score - min_s) / (max_s - min_s + 1e-9)
return sorted(all_results, key=lambda x: x.score, reverse=True)[:limit]
Comparison of deduplication methods for document deduplication:
| Method | Precision | Speed | Usage |
|---|---|---|---|
| Embedding cosine (CrossEncoder) | 95% | 10-50 ms per pair | Final ranking |
| Cosine similarity of embeddings | 85% | 1-5 ms | Preliminary deduplication |
| Text matching (Shingles) | 70% | <1 ms | Fast filtering |
Common Issues and Their Solutions
- Different source response speeds: 3-second timeout for each, as in the code above.
- Score drift after index updates: resolved by retraining normalization monthly.
- Missing embeddings in some sources: we use hybrid search + LLM for search.
Connectors for Specific Sources
class ConfluenceConnector:
async def search(self, query: str, filters: dict, limit: int) -> list[SearchResult]:
# Hybrid search: vector store (Qdrant) for semantics
# + Confluence REST API for freshness
vector_results = await self.vector_store.asimilarity_search(query, k=limit)
return [self._to_result(r) for r in vector_results]
class JiraConnector:
async def search(self, query: str, filters: dict, limit: int) -> list[SearchResult]:
# JQL with text search
jql = f'text ~ "{query}" ORDER BY updated DESC'
if filters.get("project"):
jql = f'project = {filters["project"]} AND ' + jql
issues = self.jira.search_issues(jql, maxResults=limit)
return [self._issue_to_result(i) for i in issues]
class EmailConnector:
async def search(self, query: str, filters: dict, limit: int) -> list[SearchResult]:
# Search via Microsoft Graph API or IMAP
results = await self.graph_client.search_messages(
query=query,
top=limit,
select=["subject", "bodyPreview", "from", "receivedDateTime"]
)
return [self._email_to_result(r) for r in results]
Case study: An insurance company with 600 employees. 6 sources: SharePoint (180K documents), Jira, Outlook, internal document management system, precedent database, regulatory database (external API). Before implementation, a call center operator switched between 4-5 tabs when handling a client request. After: one search interface with results from all sources. Average query handling time: 4.2 min → 1.8 min. The regulatory database source via query-time federation—latency 800 ms, acceptable for this scenario. Time savings: over 100 hours per month for the department, allowing a reduction of 2 FTEs. Annual savings from reduced search time typically range from $50,000 to $100,000 per department.
How It Works: Step by Step
- Connect sources: We develop adapters for your enterprise systems (Confluence, Jira, SharePoint, databases, etc.) using their APIs.
- Configure score normalization: We set up MinMax scaling and embedding-based deduplication to unify results.
- Deploy reranker model: A CrossEncoder model re-ranks the top candidates for final relevance.
- Set up monitoring: MLOps dashboards track latency, result quality (NDCG), and embedding drift.
Personalization of Sources by Role
Different roles see relevant sources by default. For query routing, we use LangChain Expression Language (LCEL), simplifying rule configuration.
ROLE_SOURCE_CONFIG = {
"developer": ["jira", "confluence", "gitlab", "stackoverflow-internal"],
"hr": ["confluence", "email", "hr-system", "orgchart"],
"lawyer": ["contracts-db", "sharepoint", "email", "regulations"],
"support": ["confluence", "jira", "email", "crm", "knowledge-base"],
}
Deliverables: What Is Included in the Implementation
- Prototype: Working federated search for 2-3 sources within 4-6 weeks.
- Documentation: Architectural documentation and API specification.
- Connectors: Custom adapters for each source.
- Ranking module: Deduplication and ranking module (CrossEncoder).
- Monitoring: Quality and latency dashboard (MLOps).
- Training: Team training and 1 month of post-launch support.
Timelines: 2-3 sources, pilot: 4-6 weeks; full federation of 6-8 sources: 3-4 months. Cost is calculated individually and is recouped through reduced employee labor costs.
How We Guarantee Quality
With 5+ years in enterprise AI and 15+ successful deployments, our team ensures robust E-A-T. We use certified models (CrossEncoder on RoBERTa), conduct A/B testing of ranking before deployment. We implement monitoring for embedding quality drift and automatic recalculation of normalization. We guarantee NDCG@10 not lower than 0.85 after tuning.
Assess your project—contact us for a consultation. Order a pilot in 4-6 weeks, and we will show how federated search accelerates your employees' work. Get demo access to a working prototype.







