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 viasentence-transformers - Source:
embed_codefunction at lines 80-89 ofcodebase_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 |
Executing the Vector Search
The core retrieval happens in QdrantVectorStore.search_embeddings() at lines 76-87 of vector_store.py. This method:
- Retrieves the query embedding
- Calls
client.query_points()with Qdrant's ANN index - Extracts
(node_id, score)pairs from results - 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_batchfor multiple queries to amortize embedding costs - Local vs. remote: Local Qdrant (
QDRANT_DB_PATH) suits single-node deployments; useQDRANT_URLfor production clusters - Collection sizing: Each code entity (function, class, method) becomes one Qdrant point; expect ~3-5x storage overhead for HNSW index
Summary
embed_codeincodebase_rag/embedder.pyproduces 768-dimensional query vectors using OpenAI or UniXcoder modelsQdrantVectorStoreincodebase_rag/vector_store.pywraps the Qdrant client and executesquery_points()ANN searchessemantic_code_searchincodebase_rag/tools/semantic_search.pyorchestrates 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →