STACK: LOCAL RAG
CLI: Go 1.26
QDRANT: gRPC:6334
EMBED: nomic-embed (768d)
LLM: qwen2.5-coder:7b
HYBRID: BM25 + RRF
⚡

ragcodepilot local CLI

Local RAG Code Search & Citations Engine

A local semantic code search and generation CLI

Search your codebase using plain English, right from the terminal.

ragcodepilot indexes source repositories locally with Go AST function-level chunking, embeds code via Ollama (nomic-embed-text), indexes BM25 keywords, and executes server-side Reciprocal Rank Fusion (RRF) inside Qdrant — optionally generating answers with source citations via --answer.

Implementation reviewed — · revision — ↗. Evaluation dates appear with their reports.

⚡ Try RAG Playground Get started locally 🗺️ Inspect Architecture
go run ./cmd/ragcodepilot search --answer "how does chunking work?"
RAG STATUS & CAPABILITY OVERVIEW
SEARCH DEFAULT · --answer OPT-IN
Default path is retrieval only • Retrieval: Hybrid BM25 + Dense RRF • Optional LLM: qwen2.5-coder:7b
Try scripted demo 🚀 Eval Report (— Hit@5) 📊
RETRIEVAL HIT@5
—
—
RECALL@10 (GOLDEN)
—
—
HYBRID P95 LATENCY
—
Recorded retrieval time: embedding, validation, and Qdrant lookup
DEFAULT DEPLOYMENT
Local
Host CLI + Ollama + Docker Qdrant. Offline after setup and model downloads.

Implemented, measured, still unverified

Content reviewed —. The implementation revision and experiment revisions are separate: a website refresh is not a new benchmark or release certification.

Implemented

Local CLI with hybrid retrieval, opt-in answers, and index --watch.

  • Dense-cache reuse keyed by exact enriched input, representation, and embedder identity.
  • Durable run markers, collection writer ownership, source-change detection, and indexing metrics.
  • Declaration-unique Go chunk IDs, stale-point cleanup, nested .gitignore, and actionable errors.

Inspect indexing implementation ↗ · File selection ↗

Measured

Saved self/chi recheck: —, source —.

  • Point survival: self —; chi —.
  • Self hit@5: —; chi: —. File hits are not complete context or correct answers.
  • Small cache probe: —. Smoke evidence only.

See results and known failures ↓

Still unverified

  • Full pinned self/chi clean-versus-warm indexing measurements.
  • Retry output and final point-set equivalence against a clean rebuild on those corpora.
  • Answer faithfulness, broader-repository generalization, and large-corpus capacity.

In-place updates are not atomic snapshots. Sparse refresh and upserts remain scope-wide. These are acceptance boundaries, not claims that the mechanisms are absent.

Open acceptance gates ↗

🗺️ Interactive Pipeline Architecture

STEP-BY-STEP FLOW

Start with where the work runs, then select a stage to inspect its input, output, and a simplified code example.

HOST PROCESS

Go CLI

Reads source files, chunks and enriches code, builds BM25 vectors, and orchestrates search. Prints ranked code by default.

HTTP → Ollama · gRPC → Qdrant

LOCAL MODEL SERVICE

Ollama · :11434

/api/embed creates dense vectors. With --answer, /api/chat generates text from retrieved chunks.

Sparse-only search skips embeddings.

DOCKER SERVICE

Qdrant · gRPC :6334

Persists vectors and raw source payloads. Applies filters and performs dense/sparse retrieval and hybrid RRF fusion.

Lookup and fusion share one query RPC.

Indexing writes code_chunksShared Qdrant collection: dense + sparse + raw code payload Search reads

The diagrams show logical data flow, not concurrent Go workers. Search returns code directly; the separate --answer branch sends context to the LLM. Selecting a diagram does not change the simulator below.

Source / AST Chunks 768d Dense Vectors BM25 Sparse Weights RRF Fused Context Grounded Citations
STREAM: Ingesting repository files ➔ SHA-256 Check ➔ AST Function Chunker...

    1. File Walker & Filter

    Applies language, hidden-file, and config.yaml exclusions, then Git's nested .gitignore rules. Negation cannot restore paths already excluded by configuration or hidden-file rules. Default exclusions include .git, vendor directories, and *_test.go.

    SOURCE SYMBOL
    WalkFiles(root string)
    GO PACKAGE
    internal/ingest/walker.go
    INPUT DATA
    Repository root path
    OUTPUT CONTRACT
    Filtered []string file paths

    Simplified example · View implementation ↗

    // Code preview will render here

    ⚡ RAG Simulator (scripted)

    NOT A LIVE INDEX

    Five scripted examples show which stages run in each mode. Rankings, vectors, and answers are illustrative; this page does not call Qdrant or Ollama. Unsupported queries return no demo.

    Simulated only. Modes rank a fixed illustrative candidate set. Reference numbers identify example sources, not result ranks. No timings are measured here.

    Preset Queries:
    Search Mode:
    RAG Answer Mode:
    Language Filter:

    1. DENSE VECTOR 768D
    [0.021, -0.054, 0.118, 0.082, -0.192, ... +763 floats]
    2. BM25 SPARSE SNOWBALL
    Query tokens carry uniform weights of 1.0.
    3. RRF FUSION K=60
    Sum 1/(60 + position); first position = 0.
    4. RETRIEVAL STATUS SUCCESS (3 HITS)
    Not measured. This page runs no model or database requests.
    🤖 Example answer Ollama qwen2.5-coder:7b
    Temp: 0.0 (Greedy)
    Loading answer...
    Retrieved Sources & Citations:

    Retrieved Source Chunks

    3 Results Ranked

    Code excerpts are simplified for explanation. Use each source link to inspect the implementation at the documented revision.

    🧩 Follow one function through the system

    The same ChunkFile function moves from a local file to a cited answer. Source text is real; the abbreviated payload and answer below are teaching examples.

    1. 1. Read & chunk

      The walker selects internal/ingest/chunker.go. The Go parser retains the function and its doc comment, lines 20–28, as one named chunk.

      // ChunkFile reads a source file and splits it into chunks.
      // For Go files, it uses go/parser to produce function-level chunks.
      // For all other languages, it falls back to a line-based sliding window.
      func ChunkFile(filePath, repoRoot, repo string, chunkSize, overlap int, cfg *config.Config) ([]model.CodeChunk, error) {
          if cfg.DetectLanguage(filePath) == "go" {
              return chunkGoFile(filePath, repoRoot, repo, chunkSize, overlap, cfg)
          }
          return chunkGeneric(filePath, repoRoot, repo, chunkSize, overlap, cfg)
      }
    2. 2. Add context for embedding

      Prefix the code with this header. The enriched text is used for dense embeddings and sparse document vectors; the original code remains the stored payload.

      File: internal/ingest/chunker.go
      Language: go
      Function: ChunkFile
      
      [the same source text from step 1]
    3. 3. Store one point in Qdrant

      The CLI reuses a validated dense vector or calls Ollama on a cache miss, builds BM25 document weights using scope-wide statistics, then upserts both over gRPC.

      collection: code_chunks
      vectors: dense [768 floats], sparse {term IDs, BM25 weights}
      payload: {repo, file_path, language: go, name: ChunkFile,
                chunk_type: function, start_line: 20, end_line: 28,
                content: original source, file_hash, index_version}
    4. 4. Retrieve the stored chunk

      Query: “where is ChunkFile defined”. Hybrid mode embeds it and tokenizes it into sparse terms with weight 1.0 each. Qdrant filters both candidate lists and fuses their positions with RRF.

      The saved September 23 post-merge self run returned this function at rank #1. Its source revision is separate from the implementation review; this is not a guarantee for other corpora.

    5. 5. Print code, or request an answer

      Default: return ranked code chunks. With --answer: warm the local generator, select up to --answer-limit chunks (default 5), build the prompt, and call Ollama /api/chat.

      “ChunkFile is defined in internal/ingest/chunker.go. It dispatches Go files to the AST chunker and other files to the generic chunker. [1]”

      [1] Inspect the original function ↗

      Illustrative answer. Citations make evidence inspectable; their presence alone does not prove the explanation is correct.

    Success: index → retrieve → code results → optional cited answer.

    🔬 Technical Deep Dives

    ENGINEERING DECISIONS

    Explore the foundational algorithms and engineering decisions that power high-precision code retrieval.

    Go AST Function-Level Chunker

    internal/ingest/chunker_go.go
    STRUCTURALLY AWARE

    Parses Go with go/parser. Keeps function/method declarations and named types/interfaces together when they fit. Declarations longer than 80 lines use sliding-window splitting; syntax errors fall back to the generic chunker.

    // AST Chunker extracts complete logical units: // - Package name // - Docstring comments // - Method receiver (e.g., (c *Client)) // - Function name & signature // - Full function body from { to } // - Large declarations can still cross syntax boundaries when split.

    Generic Sliding Window

    internal/ingest/chunker.go
    GENERIC FALLBACK

    Used for languages without dedicated AST parsers (Python, TypeScript, Shell). Slices files into 40-line windows with a 5-line overlap, using regex heuristics to extract surrounding identifiers.

    // 40-line sliding window + 5-line overlap (DefaultChunkLines/DefaultChunkOverlap) // Regular expression fallback identifies: // - func / def / fn definitions // - Overlap retains some neighboring context // - It cannot guarantee intact syntax or a relevant search hit

    Raw Code Chunk

    Before Enrichment
    RAW TEXT

    Embedding raw code without context leads to false positives when functions use common verbs or variables like Search or Init.

    func (c *Client) EnsureCollection(ctx context.Context, name string, vectorSize uint64) error { // ... checks collection existence ... }

    Enriched Embedding Input

    internal/ingest/enrichment.go
    STRUCTURAL CONTEXT

    Prepending the file path, language, and chunk type/name gives the embedding input explicit context. Repository and signature fields are not added to this header; a signature may already appear in the raw code.

    File: internal/qdrant/client.go Language: go Function: EnsureCollection func (c *Client) EnsureCollection(ctx context.Context, name string, vectorSize uint64) error { ... }

    Why Hybrid Search? Reciprocal Rank Fusion (RRF) with k=60

    Dense vector embeddings excel at semantic similarity (e.g. matching "how does chunking work?" with ChunkFile), but struggle with exact symbol navigation (e.g. "SearchWithTimings"). BM25 complements them with identifier and keyword matches. RRF fuses them without needing fragile score calibration:

    Score(chunk) = Σm ∈ {dense, sparse}   [ 1 / ( 60 + Positionm(chunk) ) ]
    DENSE COSINE

    Ollama 768d vector captures natural concepts, high-level intent, and synonyms.

    SPARSE BM25

    Split identifier tokens, preserved full identifiers, and additive stems support keyword matching.

    SERVER-SIDE RRF

    Lookup and fusion run inside Qdrant in one query RPC; the CLI still handles encoding and transport.

    Work through the ranks

    Illustrative candidate lists: dense favors a conceptual overview; sparse favors the exact ChunkFile symbol. Switch modes to compare the resulting order.

    Invented ranks, calculated scores — not evaluation data
    CandidateDense positionSparse positionRRF calculationResult rank

    Qdrant uses zero-based positions: first = 0. With this project's k=60, a first-place contribution is 1/60. An absent candidate contributes 0. Qdrant's formula ↗

    Incremental Reindexing & SHA-256 Change Detection

    Changed runs re-chunk the selected repository/language scope and refresh BM25 statistics and complete-point upserts. Dense vectors are reused from cache when exact enriched input and representation/model identity match; only cache misses call Ollama. Dense reuse does not make every indexing stage proportional to the edit.

    1. SHA-256 HASHING

    Each file's SHA-256 is compared with Qdrant payloads. If hashes and index version all match, indexing is a no-op.

    2. DENSE CACHE & IDENTITY

    The CLI's representation includes chunk settings and embedder identity; Ollama supplies a model digest. Cached vectors are validated before reuse. Declaration identity distinguishes same-name Go methods; version migration removes old IDs.

    3. --watch (same pipeline)

    index --watch debounces fsnotify events and re-runs the full index pipeline — not a per-file embed-only daemon.

    A collection writer lock prevents overlapping local indexers. Durable markers record incomplete or failed runs; end-of-run source verification rejects a changed source snapshot. Writes remain visible in place, and full retry-versus-clean-rebuild acceptance is still open. Inspect run state ↗ and dense cache ↗.

    📊 Retrieval Evaluation Scoreboard

    —

    Measured via ragcodepilot eval --mode hybrid — run —. Inspect saved retrieval report ↗. Self-dogfood on this repo, not a public IR benchmark. Retrieval metrics do not measure answer correctness or generation latency.

    HIT@1
    —
    Top result is a relevant chunk
    HIT@5
    —
    Relevant chunk in top 5
    MRR@5
    —
    Mean Reciprocal Rank
    RECALL@10
    —
    Share of expected files in top 10
    NEG PASS RATE
    —
    —

    Measured on self + one external Go repository

    The saved post-merge recheck used source —, not the October implementation-review date. No labels or thresholds were changed to hide known failures.

    • Self: —; —.
    • chi v5.2.3: hit@5 —, but —.
    • —. Finding a file can still miss the required declaration.

    chi retrieval report ↗ · Point survival, coverage, and failures ↗ · Run provenance ↗

    Separate indexing smoke measurement

    Probe dated —: —; —. The clean corpus had one file/two chunks; the warm corpus had two files/four chunks. This is not a controlled speedup claim, corpus benchmark, or capacity result. Sparse vectors and upserts still covered every current chunk.

    Raw indexing metrics ↗ · Probe scope and remaining gates ↗

    Generation is a separate cost: the saved — from — with --answer-limit 5 measured — p50 and — p95 for generation. This older, different workload is not an end-to-end latency comparison with the post-merge retrieval run. Inspect saved answer report ↗.

    Golden Dataset Benchmark Queries

    —

    Query String Type Top Returned File Outcome (Hybrid RRF)

    🚀 Run it on your machine

    Install Go —, Git, Docker with Compose, and Ollama first. Start Docker and the Ollama app/service. Commands below run in your terminal, not in this website.

    1. 1. Get the source

      If you already have this repository, open its root directory and skip cloning.

      git clone https://github.com/thuyvy3110/ragcodepilot.git
      cd ragcodepilot
    2. 2. Start services & download embeddings

      Qdrant runs in Docker; Ollama runs on the host. If Ollama is not already running, start ollama serve in a separate terminal. Downloads require internet access.

      docker compose up -d qdrant
      ollama pull nomic-embed-text
      go run ./cmd/ragcodepilot version
    3. 3. Index this repository

      Wait for indexing to finish before searching. This creates the default code_chunks collection using Go source files.

      go run ./cmd/ragcodepilot index --language go .
    4. 4. Retrieve code

      Hybrid search returns ranked source chunks, file paths, and line ranges. It does not need a generative model.

      go run ./cmd/ragcodepilot search "how does chunking work?"
    5. 5. Optional: generate an answer

      Download the additional model, then use --answer. The first call can take longer while the model loads. Inspect its source citations to judge the answer.

      ollama pull qwen2.5-coder:7b
      go run ./cmd/ragcodepilot search --answer "how does chunking work?"
    Service errors or an empty search?
    • Source changed during indexing: stabilize the source and retry. Writer ownership conflict: wait for the active indexer; do not delete a live lock.
    • Ollama connection refused: start the Ollama app or ollama serve. Run ollama list to check downloaded models.
    • Qdrant connection refused: check docker compose ps and docker compose logs qdrant. The CLI uses port 6334.
    • Missing collection or no results: finish indexing first and use matching collection/language/repository filters.
    • Vector dimension mismatch: use the same embedding model for index and search, or deliberately rebuild for the new model.

    Full CLI reference and configuration ↗

    💻 Interactive Terminal & CLI Explorer

    TRY CLI COMMANDS

    Switch tabs for abbreviated, illustrative output. These examples do not execute commands or measure performance. Run the copied commands locally after setup.

    bash — ragcodepilot (darwin/arm64)