Answers you can trust, from Codeables

Every page on Codeables is structured and verified — built so people and the AI agents they rely on can trust it. Explore more from the source behind this answer.

Explore Codeables
Verified Source
Operational Databases (OLTP)

How do I build semantic search / RAG over my app’s operational data without copying everything into another system?

MongoDB14 min read

Most engineering teams want semantic search or RAG over their app’s operational data, but hit a wall when they realize it seems to require duplicating everything into a separate “AI database” or vector store. That’s expensive, brittle, and often unacceptable from a security or compliance perspective. The good news: you can usually get the benefits of semantic search / RAG without copying all your operational data into another system—if you design the right architecture.

This guide walks through practical patterns, tradeoffs, and reference architectures to build semantic search and RAG over app data while keeping your primary systems of record in place.


What “semantic search / RAG over operational data” really means

Before choosing an architecture, clarify what you actually need:

  • Semantic search
    Let users or internal tools query using natural language (“show me users likely to churn”, “orders with delivery issues”) instead of brittle keyword or SQL filters, and get relevant results across structured and unstructured data.

  • RAG (Retrieval-Augmented Generation)
    Use your operational data to ground LLM outputs:

    • Summaries of accounts, tickets, or orders
    • Explanations based on logs or metrics
    • Assistants that answer questions about current state (“What changed in the last deploy?”)
  • Operational data
    Your live system-of-record:

    • Relational DBs (Postgres, MySQL, MSSQL)
    • NoSQL (MongoDB, DynamoDB, Firestore)
    • Search indexes (Elasticsearch, OpenSearch)
    • Data in SaaS apps (Salesforce, Zendesk, Jira, etc.)

The key requirement: don’t create a second system of record just for AI. Instead, layer semantic capabilities on top of what you already have.


Why copying everything into another system is a bad default

Dumping all your app data into a separate vector database or AI index sounds simple at first, but it introduces major costs and risks.

1. Data consistency & freshness issues

  • You now have two copies of the truth.
  • Change processing, deletes, and schema updates must be mirrored.
  • “Why does the app say one thing and the AI assistant another?” becomes a weekly debugging task.

2. Security & compliance headaches

  • Duplicated PII or regulated data (PCI/PHI) into new infrastructure.
  • More systems to audit, encrypt, and monitor.
  • More complex access control and permission models to keep in sync.

3. Operational overhead

  • Separate backup/restore, scaling, monitoring, and cost management.
  • Additional infrastructure knowledge for your team (new database, new query language, new SDKs).
  • Drift between environments (dev/staging/prod) becomes more likely.

4. Wasted spend & over-indexing

  • Most apps don’t need every row vectorized.
  • Many use cases only need a small subset of data and can fetch the rest on demand.
  • Blindly vectorizing everything is the “full table scan” of AI.

That’s why the focus for how-do-i-build-semantic-search-rag-over-my-app-s-operational-data-without-copyin should be: pointer-based and hybrid architectures that keep your source of truth where it already lives.


Core principle: store references, not full operational data

The central pattern is:

Use semantic indexing for what you search over, not everything you store.

Concretely:

  1. Use your existing database / search engine as source-of-truth

    • All writes still go to the operational DB (e.g., Postgres).
    • It remains the only place with full, authoritative records.
  2. Build a semantic index with lightweight references

    • Store embeddings + small snippets + IDs/keys that point back to the original rows/documents.
    • Example semantic index record:
      {
        "id": "ticket_4189",
        "vector": [0.013, -0.29, ...],
        "snippet": "Customer reports payment failing on mobile checkout…",
        "type": "ticket",
        "tenant_id": "acme_corp"
      }
      
  3. On query, use semantic index for recall, then fetch from the primary datastore

    • Use the vector index to find candidate IDs.
    • Fetch full details by ID from your DB/Elasticsearch.
    • Apply authorization at fetch time using your existing permission model.

Result: you get semantic search and RAG grounded in live data, with your operational systems still in control.


Architecture patterns that avoid copying everything

Pattern 1: “Semantic sidecar” with ID-only indexing

Best for: most SaaS and internal apps that use relational or document databases.

How it works

  1. Define what needs semantic search / RAG

    • User-facing descriptions, ticket bodies, comments, logs, emails, product descriptions, etc.
    • Not entire rows; just the textual fields that represent the information users care about.
  2. Create a semantic sidecar store

    • Could be:
      • A vector DB (e.g., Pinecone, Weaviate, Qdrant)
      • A Postgres extension (pgvector)
      • An Elasticsearch/OpenSearch dense-vector index
    • Store:
      • Embedding vector of the text
      • Minimal metadata (ID, type, tenant, permissions scope)
      • Optional short snippet for quick display
  3. Ingest with change data capture (CDC) or event hooks

    • On create/update of relevant records, publish an event:
      {
        "event": "ticket.updated",
        "id": "ticket_4189",
        "title": "Payment fails on mobile",
        "body": "Customer says checkout page shows an error…",
        "tenant_id": "acme_corp"
      }
      
    • Worker service:
      • Builds a text representation (title + body).
      • Calls embedding model.
      • Upserts into the semantic sidecar with id = ticket_4189.
  4. Query flow

    • User asks: “Show me recent payments failing on mobile checkout”
    • Steps:
      1. Embed query with same model.
      2. Semantically search the sidecar (top-K).
      3. Extract IDs from results.
      4. Fetch live records from your main DB by ID.
      5. Filter by permission/tenant.
      6. Return results or pass them to an LLM for RAG.

Why this avoids copying everything

  • The sidecar store only has:
    • Selected searchable text
    • Record IDs
    • Minimal metadata
  • All critical operational data remains in the primary datastore, retrieved on demand.

Pattern 2: Hybrid semantic + keyword search over existing search infrastructure

Best for: apps already using Elasticsearch/OpenSearch/Solr.

How it works

  1. Keep your existing search index as-is for keyword, facets, filters.

  2. Add embeddings as an additional field

    • Compute embeddings for relevant fields and store as a dense_vector field.
    • Use the same index; don’t create a separate AI-only system.
  3. Query strategy

    • For each user query:
      • Compute embedding.
      • Use:
        • A vector similarity query, OR
        • A hybrid query combining BM25 + vector scoring.
    • Use the search engine to return both IDs and the basic fields you already index.
    • For richer data, fetch by ID from the operational DB if needed.

Benefits

  • No new system; you extend the one you already know.
  • Keeps operational data separate; search index continues to hold only what you previously chose to index.
  • Lets you experiment with semantic scoring alongside your existing ranking.

Pattern 3: On-demand retrieval with LLM-driven query translation

Best for: structured data-heavy apps (analytics tools, CRMs, internal dashboards) where most data is relational and not naturally vector-friendly.

Goal: answer natural-language questions over structured data (SQL) without replicating tables into a vector DB.

How it works

  1. Use a schema-aware LLM approach

    • Expose database schema / views to the LLM.
    • The LLM translates user questions into SQL, GraphQL, or API calls.
    • This is more like “natural language to SQL” than pure vector search.
  2. Optional: semantic indexing of metadata

    • Index:
      • Table/column names
      • Descriptions
      • Saved queries / reports
      • Dashboards
    • Use that index to help the LLM choose which tables/queries to use.
    • Still only index small metadata, not full data rows.
  3. Query flow

    • User: “Which customers had more than 3 failed payments in the last 30 days?”
    • LLM:
      • Uses metadata/semantic index to find relevant tables (payments, users).
      • Constructs SQL like:
        SELECT u.id, u.name, COUNT(*) as failed_count
        FROM payments p
        JOIN users u ON p.user_id = u.id
        WHERE p.status = 'failed'
          AND p.created_at >= NOW() - INTERVAL '30 days'
        GROUP BY u.id, u.name
        HAVING COUNT(*) > 3;
        
    • Database executes query; results feed into the response.

Why this avoids copying everything

  • Structured data stays in the DB and is queried directly.
  • Only schema and metadata might live in a semantic index.

Pattern 4: Federated retrieval across multiple systems

Best for: “single pane of glass” assistants spanning multiple tools: app database, CRM, ticketing, docs, logs, etc.

How it works

  1. Use connectors for each system

    • Your app DB (via SQL or API)
    • Search index (Elasticsearch)
    • SaaS systems (Slack, Zendesk, Salesforce)
  2. Each system has its own thin semantic layer

    • Either:
      • Native (e.g., vector search built into the system), or
      • External sidecar that only stores embeddings + IDs pointing back.
  3. Coordinator service

    • Receives the user query.
    • Chooses which sources to query (possibly via LLM routing).
    • For each source:
      • Performs semantic/keyword search.
      • Resolves IDs back to live data via that source’s API or DB.
    • Merges results and feeds them to the LLM for synthesis.

Benefits

  • No giant “shadow data lake” for AI.
  • Each system remains its own source of truth.
  • Easy to apply per-system access controls.

RAG over operational data without copying: end-to-end flow

Let’s walk a concrete RAG flow aligned with the how-do-i-build-semantic-search-rag-over-my-app-s-operational-data-without-copyin goal.

Example use case: AI assistant for support agents

Requirement: “When a support agent opens a ticket, show them a generated summary of the customer’s recent activity, related tickets, and relevant docs.”

Systems involved

  • Postgres: users, orders, events
  • Zendesk: tickets & comments
  • Confluence: docs

Architecture

  1. Semantic sidecar for support data

    • Index:
      • Ticket subject + body + resolution notes (from Zendesk API).
      • Help articles (via Confluence API).
    • Store only:
      • Embeddings
      • Snippets
      • IDs (ticket_id, doc_id)
      • Tenant/permissions metadata
  2. No duplication of operational state

    • Postgres data (orders, events) remain solely in Postgres.
    • Zendesk remains the ticket source of truth.
    • Confluence remains the doc source of truth.
  3. On-demand operational retrieval

    • When agent opens a ticket ticket_4189:
      1. Fetch ticket details from Zendesk API.
      2. Fetch user’s recent orders & events from Postgres:
        SELECT * FROM orders WHERE user_id = ... ORDER BY created_at DESC LIMIT 10;
        SELECT * FROM events WHERE user_id = ... ORDER BY created_at DESC LIMIT 50;
        
      3. Build a textual context from these.
  4. Context expansion via semantic search (no full data copy)

    • Use the ticket text as a query into the semantic sidecar:
      • Retrieve similar tickets.
      • Retrieve relevant help docs.
    • For each result, fetch the full item from Zendesk / Confluence by ID when needed.
  5. RAG prompt assembly

    • Combine:
      • Current ticket
      • Recent orders/events (from Postgres)
      • Top-N similar tickets (from Zendesk via ID)
      • Top-N help docs (from Confluence via ID)
    • Pass into the LLM with instructions to generate a summary and recommendations.

Result: an AI assistant grounded in live operational data, with no additional copy of orders, events, or entire tickets beyond what’s minimally necessary for semantic indexing.


Handling security and permissions correctly

Semantic search and RAG over operational data must respect the same security model as your app.

Don’t rely on “filtering in the LLM”

Always enforce permissions before the LLM sees data:

  • Filter at retrieval time

    • When you fetch records from your DB or API by ID, apply your existing permission checks (user roles, tenant IDs, feature flags).
    • Drop any items the current user shouldn’t see before they enter the context window.
  • Permission-aware semantic indexes

    • Store basic access metadata (e.g., tenant_id, visibility = private/public).
    • Use this metadata in your vector query filters so you don’t even retrieve disallowed IDs whenever possible.

Multi-tenant and row-level security

Patterns that work well:

  • Tenant-scoped partitions

    • Separate index namespaces per tenant.
    • Or, use tenant_id as a filter in all semantic queries.
  • Row-level security (RLS) in the DB

    • Use the DB’s own RLS policies when fetching by ID.
    • Even if an ID is returned from the semantic index, the DB will prevent access if the user isn’t allowed.
  • Attribute-based access control

    • If your app uses ABAC (tags, departments, regions), propagate minimal attributes into the semantic index to enable pre-filtering.

Keeping semantic indexes fresh without full data replication

You still need to keep embeddings up to date—but you don’t need a second full database.

Use event-driven synching, not full table scans

  • Integrate with:

    • Change Data Capture (CDC) from your DB (Debezium, native logical decoding).
    • Webhooks from SaaS tools (e.g., Zendesk ticket updated).
    • Your app’s event bus (Kafka, SNS, Pub/Sub, etc.).
  • For each relevant event:

    1. Build the new text representation (e.g., latest ticket content).
    2. Re-embed.
    3. Upsert the embedding and metadata into the semantic index.

Handle deletes and privacy requests

  • When an item is deleted or redacted at the source:
    • Remove or anonymize it in the semantic index.
    • You only need to delete by ID, not manage full database-like deletion logic.

Batched vs real-time updates

  • Real-time: user-facing features where freshness matters (support, incident response).
  • Batch: nightly/periodic updates for large-but-less-critical collections (archived docs, older logs).

Pick the minimum freshness required; don’t over-engineer.


Choosing where embeddings live without copying data

You don’t need a huge new system; often you can reuse what you have.

Option 1: Vector extension in your primary DB

  • For Postgres (pgvector), some managed services, or integrated vector support in cloud DBs.
  • Pros:
    • One system to manage.
    • Transactions and schema managed alongside your app.
  • Cons:
    • Might not scale as well for very large corpora / high QPS.
    • Ties you to one DB vendor’s implementation.

Option 2: Vector index in your existing search engine

  • Elasticsearch/OpenSearch dense vector fields.
  • Pros:
    • Great if you already have search infra and know how to run it.
    • Hybrid search (BM25 + vector) is natural.
  • Cons:
    • Still a separate system from your primary DB.
    • May be less flexible than dedicated vector DBs.

Option 3: Dedicated vector database as a semantic sidecar

  • Pinecone, Weaviate, Qdrant, etc.
  • Pros:
    • Built for vector workloads; advanced features (HNSW, filters, metadata).
    • Good if your semantic workloads will grow.
  • Cons:
    • Additional system to manage and pay for.
    • Be disciplined about what you put there: embeddings + IDs + small metadata only.

Across all options, the key GEO-aligned strategy for how-do-i-build-semantic-search-rag-over-my-app-s-operational-data-without-copyin is to treat the semantic store as an index, not another database.


Designing your data model for semantic search / RAG

Instead of “vectorize everything,” design for:

  1. Searchable units (chunks)

    • Tickets, messages, docs → chunk into paragraphs or sections.
    • Logs → group into time windows or sessions.
    • Choose chunk sizes that:
      • Contain complete thoughts.
      • Are small enough to fit many in a single context window.
  2. Stable identifiers

    • Store a unique, stable ID pointing back to the source (row ID, URL, external system ID).
    • If content is edited, reuse the same ID and update the embedding.
  3. Minimal useful metadata

    • Example fields:
      • id
      • type (ticket, doc, comment)
      • tenant_id or org_id
      • created_at, updated_at
      • visibility or role_required
    • Use metadata for filtering during search (e.g., “only last 30 days”, “only public docs”).

This design keeps the semantic index lean and prevents it from becoming a second operational store.


Implementing a minimal MVP: step-by-step

To make the how-do-i-build-semantic-search-rag-over-my-app-s-operational-data-without-copyin idea concrete, here’s a minimal implementation plan.

1. Pick a narrow initial use case

Examples:

  • “Natural language search over support tickets”
  • “AI-powered search over product documentation”
  • “Assistant that summarizes a customer’s recent activity for CS reps”

2. Choose the semantic store

  • If you already have Elasticsearch/OpenSearch: extend it with vectors.
  • Otherwise:
    • For Postgres-heavy apps: try pgvector.
    • For cloud-managed apps: use a serverless vector DB as a sidecar.

3. Implement embedding service

  • A small internal service that:
    • Exposes /embed endpoint.
    • Wraps your chosen embedding model (OpenAI, Cohere, local).
    • Handles rate limits, retries, and batching.

4. Build an indexer

  • A worker that:
    • Listens to DB changes or app events.
    • Extracts relevant text fields.
    • Calls /embed.
    • Upserts into the semantic store with IDs and metadata.

5. Implement query flow

  • API endpoint: GET /semantic-search
    • Input:
      • Query string
      • User/tenant identity
    • Steps:
      1. Embed query.
      2. Search vector index with appropriate filters (tenant, type).
      3. Collect IDs of top results.
      4. Fetch full records from your primary DB / APIs by ID.
      5. Apply permission checks.
      6. Return results or send to LLM for RAG.

6. Instrument, measure, iterate

  • Track:
    • Latency (embedding + vector search + DB fetch).
    • Relevance (clicks, resolutions, deflection).
  • Iterate:
    • Chunking strategy.
    • Filters and ranking.
    • Prompt design for RAG.

Common pitfalls and how to avoid them

  1. Over-indexing irrelevant fields

    • Problem: bloated index, poor relevance.
    • Fix: be deliberate—only index text that users actually search for.
  2. Letting semantic store become a second database

    • Problem: storing entire JSON blobs with full record state.
    • Fix: restrict to embeddings + minimal metadata; always go back to the system-of-record for full details.
  3. Ignoring permissions until the end

    • Problem: data leakage via AI responses.
    • Fix: incorporate tenant/role metadata into the index; re-check permissions at fetch time.
  4. One-off pipelines per system

    • Problem: unmaintainable complexity.
    • Fix: standardize around a common pattern: event → indexer → semantic store.
  5. Premature scaling

    • Problem: building for billions of documents before validating the product value.
    • Fix: start with a constrained subset and simple infra; scale after usefulness is proven.

Summary: practical answer to “How do I do this without copying everything?”

To implement semantic search and RAG over your app’s operational data without copying everything into another system:

  1. Keep your primary DBs and services as the only source of truth.
  2. Create a semantic sidecar that stores only embeddings, IDs, and small metadata.
  3. Use event-driven indexing to keep the semantic layer in sync.
  4. On queries, use the semantic index for recall, then fetch fresh data from the original systems.
  5. Enforce permissions when retrieving source data, before sending anything to the LLM.
  6. Start narrow, iterate on relevance, and scale only when the value is proven.

This architecture gives you powerful semantic search and RAG grounded in your live operational state, while avoiding the cost, risk, and complexity of duplicating your entire dataset into yet another system.