How Semantic Search Uses Qdrant for Vector-Based Code Retrieval in code-graph-rag

Semantic search in code-graph-rag embeds natural language queries as vectors and searches a Qdrant collection to retrieve the most similar code nodes, then fetches full metadata from the graph database.

The code-graph-rag repository implements a hybrid retrieval system that combines vector similarity search via Qdrant with graph-based metadata retrieval. This article explains exactly how the semantic_code_search tool transforms a plain-text query into relevant code results using Qdrant's approximate nearest neighbor (ANN) capabilities.

Embedding the Query with embed_code

Every semantic search begins with vectorization. The semantic_code_search function calls embed_code from codebase_rag/embedder.py to convert the natural language query into a dense embedding:

  • Output dimension: 768-dimensional vector (for UniXcoder)
  • Supported providers: OpenAI (text-embedding-3-small) or UniXcoder via sentence-transformers
  • Source: embed_code function at lines 80-89 of codebase_rag/embedder.py
from codebase_rag.embedder import embed_code

query = "how to parse JSON from a string in Python"
query_embedding = embed_code(query)  # Returns: numpy array (768,)

The embedding model is configured via settings.EMBEDDING_PROVIDER and loaded lazily for reuse across requests.

Configuring Qdrant as the Vector Store Backend

The repository uses an abstraction layer that supports multiple vector stores, with Qdrant as the default.

In codebase_rag/constants/providers.py, the VectorStoreBackend enum defines available backends:

class VectorStoreBackend(str, Enum):
    QDRANT = "qdrant"
    CHROMA = "chroma"  # Optional alternative

The global settings.VECTOR_STORE_BACKEND defaults to VectorStoreBackend.QDRANT. When _get_vector_store() is invoked in codebase_rag/vector_store.py (lines 30-38), it instantiates a QdrantVectorStore object.

Initializing the Qdrant Client

The QdrantVectorStore manages client lifecycle through get_qdrant_client() at lines 50-68 of vector_store.py:

def get_qdrant_client() -> QdrantClient:
    """Lazily create Qdrant client with auto-collection setup."""
    if settings.QDRANT_URL:
        client = QdrantClient(url=settings.QDRANT_URL, api_key=settings.QDRANT_API_KEY)
    else:
        client = QdrantClient(path=settings.QDRANT_DB_PATH)  # Local mode

    
    # Ensure collection exists with correct vector size

    _ensure_collection(client)
    return client

Configuration parameters (from codebase_rag/config.py):

Setting Purpose Default
QDRANT_URL Remote Qdrant server URL None (local mode)
QDRANT_DB_PATH Local database directory "./qdrant_db"
QDRANT_COLLECTION_NAME Target collection "code_embeddings"
VECTOR_SIZE Embedding dimension 768

The core retrieval happens in QdrantVectorStore.search_embeddings() at lines 76-87 of vector_store.py. This method:

  1. Retrieves the query embedding
  2. Calls client.query_points() with Qdrant's ANN index
  3. Extracts (node_id, score) pairs from results
  4. Optionally filters by project prefix

# Inside vector_store.py — QdrantVectorStore.search_embeddings()

def search_embeddings(
    self,
    query_embedding: list[float],
    top_k: int = 10,
    project: str | None = None,
) -> list[tuple[str, float]]:
    client = get_qdrant_client()
    
    result = client.query_points(
        collection_name=settings.QDRANT_COLLECTION_NAME,
        query=query_embedding,
        limit=top_k,
        with_payload=True,  # Retrieve stored metadata

    )
    
    # Extract node IDs and similarity scores

    matches = [
        (point.payload["node_id"], point.score)
        for point in result.points
        if not project or point.payload.get("qualified_name", "").startswith(project)
    ]
    return matches

The payload stored with each Qdrant point contains:

  • node_id — Unique identifier for the graph node (primary key)
  • qualified_name — Fully qualified symbol name (e.g., mypackage.module.function)
  • project — Repository/project prefix for filtering

Mapping Vectors Back to Graph Nodes

Raw Qdrant results lack full code context. The semantic_code_search tool at lines 41-48 of codebase_rag/tools/semantic_search.py bridges this gap by querying the graph database:


# Inside tools/semantic_search.py

from codebase_rag.query_builder import build_nodes_by_ids_query

def semantic_code_search(ingestor, query: str, top_k: int = 5, project: str | None = None):
    # 1. Embed query

    query_embedding = embed_code(query)
    
    # 2. Search Qdrant

    vector_results = search_embeddings(query_embedding, top_k=top_k, project=project)
    node_ids = [node_id for node_id, _ in vector_results]
    scores = {node_id: round(score, 4) for node_id, score in vector_results}
    
    # 3. Fetch full node details from graph

    cypher_query = build_nodes_by_ids_query(node_ids)
    graph_results = ingestor.run_query(cypher_query)
    
    # 4. Combine and format

    return [
        SemanticSearchResult(
            node_id=r["id"],
            qualified_name=r["qualified_name"],
            type=r["type"],  # FUNCTION, CLASS, METHOD, etc.

            score=scores[r["id"]],
        )
        for r in graph_results
    ]

This hybrid architecture separates concerns:

Component Responsibility Technology
Embedding Convert text → dense vector OpenAI/UniXcoder
Vector search Find similar vectors fast Qdrant (HNSW index)
Graph storage Rich code metadata, relationships Neo4j or compatible backend

Complete Usage Example

from codebase_rag.tools.semantic_search import semantic_code_search

# ingestor: QueryProtocol instance with graph connection

results = semantic_code_search(
    ingestor=ingestor,
    query="async file download with retry logic",
    top_k=5,
    project="myproject",  # Optional: restrict to specific codebase

)

for r in results:
    print(f"{r.qualified_name} ({r.type}) — similarity: {r.score}")
    # Output: myproject.utils.http.download_with_retry (FUNCTION) — similarity: 0.8912

Performance and Scaling Considerations

  • Index type: Qdrant uses HNSW (Hierarchical Navigable Small World) for sub-10ms ANN queries at scale
  • Batching: Use embed_code_batch for multiple queries to amortize embedding costs
  • Local vs. remote: Local Qdrant (QDRANT_DB_PATH) suits single-node deployments; use QDRANT_URL for production clusters
  • Collection sizing: Each code entity (function, class, method) becomes one Qdrant point; expect ~3-5x storage overhead for HNSW index

Summary

  • embed_code in codebase_rag/embedder.py produces 768-dimensional query vectors using OpenAI or UniXcoder models
  • QdrantVectorStore in codebase_rag/vector_store.py wraps the Qdrant client and executes query_points() ANN searches
  • semantic_code_search in codebase_rag/tools/semantic_search.py orchestrates the full pipeline: embed → search Qdrant → fetch graph metadata → return formatted results
  • Configuration defaults to local Qdrant but supports remote servers via environment variables in codebase_rag/config.py
  • The design intentionally separates vector search (similarity) from graph retrieval (relationships and metadata) for flexibility and performance

Frequently Asked Questions

How does code-graph-rag handle Qdrant authentication for remote servers?

Remote Qdrant instances are supported via the QDRANT_URL and QDRANT_API_KEY settings in codebase_rag/config.py. When QDRANT_URL is set, get_qdrant_client() creates a QdrantClient with URL and API key rather than local path. This enables managed Qdrant Cloud or self-hosted deployments with authentication.

What embedding models does semantic_search support?

The embed_code function supports two providers configurable via settings.EMBEDDING_PROVIDER: OpenAI (text-embedding-3-small, 1536 dimensions) and UniXcoder via sentence-transformers (768 dimensions). The vector size in Qdrant is set during collection creation to match the active provider's output dimension.

Can I use a different vector store instead of Qdrant?

Yes, though Qdrant is the default. The VectorStoreBackend enum in codebase_rag/constants/providers.py allows switching to Chroma or other backends by implementing the same interface. You must update settings.VECTOR_STORE_BACKEND and ensure the alternative store's client code exists in vector_store.py.

Why does semantic search query both Qdrant and the graph database?

This separation optimizes for different access patterns. Qdrant excels at fast approximate similarity search over high-dimensional vectors but stores only lightweight payloads (node_id, qualified_name). The graph database contains rich relationships (calls, inherits, contains) and full node properties. Querying both gives accurate similarity rankings plus complete, traversable code context.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →