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, ),