From 70a3fd44ee768dd3d56354e5a7fa0239d85b8b22 Mon Sep 17 00:00:00 2001 From: Claudio Ferreira Filho Date: Sun, 3 May 2026 20:24:47 -0300 Subject: [PATCH] feat: add exclude_paths parameter to search Adds an exclude_paths parameter (list of GLOB patterns) to filter out unwanted files from search results. This is useful for projects with i18n/locales or generated files that dominate search results. The parameter is supported across all layers: - MCP tool (server.py) - CLI (--exclude flag) - Client library - Daemon - Query engine (SQL-level NOT GLOB filtering) When exclude_paths is provided, the query falls back to full-scan mode (same as paths), applying NOT GLOB clauses in the WHERE condition. Closes #146 --- src/cocoindex_code/cli.py | 4 ++++ src/cocoindex_code/client.py | 2 ++ src/cocoindex_code/daemon.py | 1 + src/cocoindex_code/project.py | 2 ++ src/cocoindex_code/protocol.py | 1 + src/cocoindex_code/query.py | 13 ++++++++++--- src/cocoindex_code/server.py | 8 ++++++++ 7 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/cocoindex_code/cli.py b/src/cocoindex_code/cli.py index 4cc0a48..00690bc 100644 --- a/src/cocoindex_code/cli.py +++ b/src/cocoindex_code/cli.py @@ -223,6 +223,7 @@ def _search_with_wait_spinner( query: str, languages: list[str] | None = None, paths: list[str] | None = None, + exclude_paths: list[str] | None = None, limit: int = 10, offset: int = 0, ) -> SearchResponse: @@ -248,6 +249,7 @@ def _on_waiting() -> None: query=query, languages=languages, paths=paths, + exclude_paths=exclude_paths, limit=limit, offset=offset, on_waiting=_on_waiting, @@ -540,6 +542,7 @@ def search( query: list[str] = _typer.Argument(..., help="Search query"), lang: list[str] = _typer.Option([], "--lang", help="Filter by language"), path: str | None = _typer.Option(None, "--path", help="Filter by file path glob"), + exclude: list[str] = _typer.Option([], "--exclude", help="Exclude file path glob(s)"), offset: int = _typer.Option(0, "--offset", help="Number of results to skip"), limit: int = _typer.Option(10, "--limit", help="Maximum results to return"), refresh: bool = _typer.Option(False, "--refresh", help="Refresh index before searching"), @@ -565,6 +568,7 @@ def search( query=query_str, languages=lang or None, paths=paths, + exclude_paths=exclude or None, limit=limit, offset=offset, ) diff --git a/src/cocoindex_code/client.py b/src/cocoindex_code/client.py index 262af87..18fefc9 100644 --- a/src/cocoindex_code/client.py +++ b/src/cocoindex_code/client.py @@ -278,6 +278,7 @@ def search( query: str, languages: list[str] | None = None, paths: list[str] | None = None, + exclude_paths: list[str] | None = None, limit: int = 5, offset: int = 0, on_waiting: Callable[[], None] | None = None, @@ -298,6 +299,7 @@ def search( query=query, languages=languages, paths=paths, + exclude_paths=exclude_paths, limit=limit, offset=offset, ) diff --git a/src/cocoindex_code/daemon.py b/src/cocoindex_code/daemon.py index 41334bc..fa82a8b 100644 --- a/src/cocoindex_code/daemon.py +++ b/src/cocoindex_code/daemon.py @@ -275,6 +275,7 @@ async def _search_with_wait( query=req.query, languages=req.languages, paths=req.paths, + exclude_paths=req.exclude_paths, limit=req.limit, offset=req.offset, ) diff --git a/src/cocoindex_code/project.py b/src/cocoindex_code/project.py index f661c21..b37f1cb 100644 --- a/src/cocoindex_code/project.py +++ b/src/cocoindex_code/project.py @@ -179,6 +179,7 @@ async def search( query: str, languages: list[str] | None = None, paths: list[str] | None = None, + exclude_paths: list[str] | None = None, limit: int = 5, offset: int = 0, ) -> list[SearchResult]: @@ -192,6 +193,7 @@ async def search( offset=offset, languages=languages, paths=paths, + exclude_paths=exclude_paths, ) return [ SearchResult( diff --git a/src/cocoindex_code/protocol.py b/src/cocoindex_code/protocol.py index b584a4d..4d9729e 100644 --- a/src/cocoindex_code/protocol.py +++ b/src/cocoindex_code/protocol.py @@ -22,6 +22,7 @@ class SearchRequest(_msgspec.Struct, tag="search"): query: str languages: list[str] | None = None paths: list[str] | None = None + exclude_paths: list[str] | None = None limit: int = 5 offset: int = 0 diff --git a/src/cocoindex_code/query.py b/src/cocoindex_code/query.py index a2991ee..5b6b698 100644 --- a/src/cocoindex_code/query.py +++ b/src/cocoindex_code/query.py @@ -51,6 +51,7 @@ def _full_scan_query( offset: int, languages: list[str] | None = None, paths: list[str] | None = None, + exclude_paths: list[str] | None = None, ) -> list[tuple[Any, ...]]: """Full scan with SQL-level distance computation and filtering.""" conditions: list[str] = [] @@ -66,6 +67,11 @@ def _full_scan_query( conditions.append(f"({path_clauses})") params.extend(paths) + if exclude_paths: + exclude_clauses = " AND ".join("file_path NOT GLOB ?" for _ in exclude_paths) + conditions.append(f"({exclude_clauses})") + params.extend(exclude_paths) + where = f"WHERE {' AND '.join(conditions)}" if conditions else "" params.extend([limit, offset]) @@ -90,6 +96,7 @@ async def query_codebase( offset: int = 0, languages: list[str] | None = None, paths: list[str] | None = None, + exclude_paths: list[str] | None = None, ) -> list[QueryResult]: """ Perform vector similarity search using vec0 KNN index. @@ -114,8 +121,8 @@ async def query_codebase( embedding_bytes = query_embedding.astype("float32").tobytes() with db.readonly() as conn: - if paths: - rows = _full_scan_query(conn, embedding_bytes, limit, offset, languages, paths) + if paths or exclude_paths: + rows = _full_scan_query(conn, embedding_bytes, limit, offset, languages, paths, exclude_paths) elif not languages or len(languages) == 1: lang = languages[0] if languages else None rows = _knn_query(conn, embedding_bytes, limit + offset, lang) @@ -131,7 +138,7 @@ async def query_codebase( key=lambda r: r[5], ) - if not paths: + if not paths and not exclude_paths: rows = rows[offset:] return [ diff --git a/src/cocoindex_code/server.py b/src/cocoindex_code/server.py index 2708c86..389c467 100644 --- a/src/cocoindex_code/server.py +++ b/src/cocoindex_code/server.py @@ -117,6 +117,13 @@ async def search( " Example: ['src/utils/*', '*.py']" ), ), + exclude_paths: list[str] | None = Field( + default=None, + description=( + "Exclude file path pattern(s) using GLOB wildcards (* and ?)." + " Example: ['i18n/locales/*', '*.min.js']" + ), + ), ) -> SearchResultModel: """Query the codebase index via the daemon.""" from . import client as _client @@ -132,6 +139,7 @@ async def search( query=query, languages=languages, paths=paths, + exclude_paths=exclude_paths, limit=limit, offset=offset, ),