Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion docs/src/content/docs/core-concepts/semantic-search.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,25 @@ def safe_semantic_search(repo_path: str, query: str, top_k: int = 5):
return repo.search_text(query)
```

## CLI Support

Semantic search is now available via the `kit search-semantic` command:

```bash
# Basic semantic search
kit search-semantic /path/to/repo "authentication logic"

# Advanced options
kit search-semantic /path/to/repo "error handling patterns" \
--top-k 10 \
--embedding-model all-mpnet-base-v2 \
--chunk-by symbols
```

See the [CLI documentation](/introduction/cli#kit-search-semantic) for complete usage details.

## Limitations & Future Plans

* **CLI support**: The CLI (`kit search` / `kit serve`) currently performs **text** search only. A semantic variant is planned.
* **Language support**: Works with any language that kit can parse, but quality depends on symbol extraction
* **Index management**: Future versions may include index cleanup, optimization, and migration tools

Expand Down
108 changes: 108 additions & 0 deletions docs/src/content/docs/introduction/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,71 @@ kit search /path/to/repo "def.*login"
kit search /path/to/repo "TODO" --pattern "*.py"
```

#### `kit search-semantic`

AI-powered semantic search using vector embeddings to find code by meaning rather than keywords. Requires `sentence-transformers` package.

```bash
kit search-semantic <repository-path> <query> [OPTIONS]
```

**Options:**
- `--top-k, -k <count>`: Maximum number of results to return (default: 5)
- `--output, -o <file>`: Save output to JSON file
- `--embedding-model, -e <model>`: SentenceTransformers model name (default: all-MiniLM-L6-v2)
- `--chunk-by, -c <strategy>`: Chunking strategy: 'symbols' or 'lines' (default: symbols)
- `--build-index/--no-build-index`: Force rebuild of vector index (default: false)
- `--persist-dir, -p <dir>`: Directory to persist vector index
- `--format, -f <format>`: Output format: 'table', 'json', or 'plain' (default: json)

**Index Management:**
The vector index is automatically built on first use and persisted for subsequent searches. Use `--build-index` to force a rebuild when the codebase changes significantly.

**Examples:**

```bash
# Find authentication-related code (builds index on first run)
kit search-semantic /path/to/repo "authentication logic"

# Force rebuild index after major code changes
kit search-semantic /path/to/repo "new feature" --build-index

# Search for error handling patterns (uses existing index)
kit search-semantic /path/to/repo "error handling patterns" --top-k 10

# Use line-based chunking for better granularity
kit search-semantic /path/to/repo "database connection" --chunk-by lines

# Use a different embedding model
kit search-semantic /path/to/repo "user registration" --embedding-model all-mpnet-base-v2

# Export results to JSON
kit search-semantic /path/to/repo "API endpoints" --output semantic-results.json

# Search without rebuilding index (if already built)
kit search-semantic /path/to/repo "payment processing" --no-build-index

# Get plain output for piping to other tools
kit search-semantic /path/to/repo "error handling" --format plain | head -5

# Get JSON output for programmatic processing
kit search-semantic /path/to/repo "database queries" --format json | jq '.[] | .file'
```

**Setup Requirements:**

```bash
# Install sentence-transformers for semantic search
pip install sentence-transformers

# Or install kit with semantic search support
pip install 'cased-kit[ml]' # minimal extras for semantic search
# Or install kit with all optional extras
pip install 'cased-kit[all]'
```

**Note:** First run will download the embedding model and build the vector index, which may take time depending on repository size.

#### `kit grep`

Perform fast literal grep search on repository files using system grep. By default, excludes common build directories, cache folders, and hidden directories for optimal performance.
Expand Down Expand Up @@ -834,6 +899,41 @@ for component in "src/auth" "src/api" "src/models"; do
done
```

### Semantic Code Discovery

Use semantic search to understand unfamiliar codebases or find implementation patterns:

```bash
#!/bin/bash
REPO_PATH="/path/to/unfamiliar/repo"

# First-time setup (builds vector index)
echo "🔍 Building semantic index..."
kit search-semantic $REPO_PATH "test setup" --top-k 1 > /dev/null

echo "🧠 Exploring codebase with semantic search..."

# Understand architecture patterns
kit search-semantic $REPO_PATH "authentication flow" --top-k 3
kit search-semantic $REPO_PATH "database connection setup" --top-k 3

# Find implementation examples
kit search-semantic $REPO_PATH "error handling patterns" --top-k 5
kit search-semantic $REPO_PATH "retry logic with backoff" --top-k 3

# Discover testing approaches
kit search-semantic $REPO_PATH "unit test examples" --top-k 5
kit search-semantic $REPO_PATH "mocking external services" --top-k 3

# Find configuration patterns
kit search-semantic $REPO_PATH "environment variable configuration" --top-k 3
kit search-semantic $REPO_PATH "logging setup" --top-k 3

# Export findings for team review
kit search-semantic $REPO_PATH "API endpoint implementation" --output api-patterns.json
kit search-semantic $REPO_PATH "data validation logic" --output validation-patterns.json
```

### AI-Powered Development

```bash
Expand Down Expand Up @@ -983,6 +1083,14 @@ jobs:
- Combine `kit commit` with `kit summarize` for complete AI-powered development workflow
- Use `--plain` output for piping to other AI tools

### Semantic Search

- Use `symbols` chunking for better semantic boundaries (default)
- Try different embedding models for different use cases: lightweight (`all-MiniLM-L6-v2`) vs. quality (`all-mpnet-base-v2`)
- Build index once, reuse with `--no-build-index` for faster subsequent searches
- Use natural language queries that describe what you're looking for conceptually
- Combine with text search (`kit search`) for comprehensive code discovery

### Scripting

- Always check command exit codes (`$?`) in scripts
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "cased-kit"
version = "1.4.0"
version = "1.5.0"
description = "A modular toolkit for LLM-powered codebase understanding."
authors = [
{ name = "Cased", email = "ted@cased.com" }
Expand Down
2 changes: 1 addition & 1 deletion src/kit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

__author__ = "cased"
__version__ = "1.4.0"
__version__ = "1.5.0"

from .code_searcher import CodeSearcher
from .context_extractor import ContextExtractor
Expand Down
189 changes: 189 additions & 0 deletions src/kit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def main(
• [cyan]dependencies[/] - Analyze & visualize code dependencies
• [cyan]symbols[/] - Extract functions, classes, etc.
• [cyan]search[/] - Find patterns across codebase
• [cyan]search-semantic[/] - AI-powered semantic code search
• [cyan]file-tree[/] - Repository structure overview

[bold magenta]🔧 Utility Commands:[/]
Expand Down Expand Up @@ -1501,6 +1502,194 @@ def find_symbol_usages(
raise typer.Exit(code=1)


@app.command("search-semantic")
def search_semantic(
path: str = typer.Argument(..., help="Path to the local repository."),
query: str = typer.Argument(..., help="Natural language query to search for."),
top_k: int = typer.Option(5, "--top-k", "-k", help="Maximum number of results to return."),
output: Optional[str] = typer.Option(None, "--output", "-o", help="Output to JSON file instead of stdout."),
embedding_model: str = typer.Option(
"all-MiniLM-L6-v2", "--embedding-model", "-e", help="SentenceTransformers model name for embeddings."
),
chunk_by: str = typer.Option("symbols", "--chunk-by", "-c", help="Chunking strategy: 'symbols' or 'lines'."),
build_index: bool = typer.Option(
False, "--build-index/--no-build-index", help="Force rebuild of vector index (default: false)."
),
persist_dir: Optional[str] = typer.Option(None, "--persist-dir", "-p", help="Directory to persist vector index."),
format: str = typer.Option("json", "--format", "-f", help="Output format: table, json, plain"),
ref: Optional[str] = typer.Option(
None, "--ref", help="Git ref (SHA, tag, or branch) to checkout for remote repositories."
),
):
"""Perform semantic search using vector embeddings and natural language queries.

This command uses vector embeddings to find code based on meaning rather than just keywords.
It requires the 'sentence-transformers' package for embedding generation.

Examples:
kit search-semantic . "authentication logic"
kit search-semantic . "error handling patterns" --top-k 10
kit search-semantic . "database connection" --chunk-by lines
kit search-semantic . "user registration" --embedding-model all-mpnet-base-v2
"""
from kit import Repository

try:
# Import sentence-transformers with helpful error message
try:
from sentence_transformers import SentenceTransformer
except ImportError:
typer.secho("❌ The 'sentence-transformers' package is required for semantic search.", fg=typer.colors.RED)
typer.echo("💡 Install it with: pip install sentence-transformers")
typer.echo("💡 Or install kit with semantic search support: pip install 'cased-kit[ml]'")
raise typer.Exit(code=1)

# Validate chunk_by parameter
if chunk_by not in ["symbols", "lines"]:
typer.secho(f"❌ Invalid chunk_by value: {chunk_by}. Use 'symbols' or 'lines'.", fg=typer.colors.RED)
raise typer.Exit(code=1)

# Initialize repository
try:
repo = Repository(path, ref=ref)
except Exception as e:
if format == "plain":
typer.echo(f"Error: {e}")
else:
typer.secho(f"Error: {e}", fg=typer.colors.RED)
raise typer.Exit(code=1)

# Load embedding model
if format not in ["plain", "json"]:
typer.echo(f"Loading embedding model: {embedding_model}")
try:
model = SentenceTransformer(embedding_model)
except Exception as e:
if format == "plain":
typer.echo(f"Failed to load embedding model '{embedding_model}': {e}")
typer.echo("Popular models: all-MiniLM-L6-v2, all-mpnet-base-v2, paraphrase-MiniLM-L6-v2")
else:
typer.secho(f"Failed to load embedding model '{embedding_model}': {e}", fg=typer.colors.RED)
typer.echo("Popular models: all-MiniLM-L6-v2, all-mpnet-base-v2, paraphrase-MiniLM-L6-v2")
raise typer.Exit(code=1)

# Create embedding function
def embed_fn(texts):
if isinstance(texts, str):
# Single string input - return single embedding
return model.encode(texts).tolist()
else:
# List of strings - return list of embeddings
return model.encode(texts).tolist()

# Get or create vector searcher
if format not in ["plain", "json"]:
typer.echo("Initializing vector searcher...")
try:
vector_searcher = repo.get_vector_searcher(embed_fn=embed_fn, persist_dir=persist_dir)
except Exception as e:
if format == "plain":
typer.echo(f"Failed to initialize vector searcher: {e}")
else:
typer.secho(f"Failed to initialize vector searcher: {e}", fg=typer.colors.RED)
raise typer.Exit(code=1)

# Check if index exists
try:
index_exists = vector_searcher.backend.count() > 0
except Exception:
index_exists = False

# Build index if requested or if it doesn't exist
if build_index or not index_exists:
if format not in ["plain", "json"]:
if build_index:
typer.echo(f"Rebuilding vector index (chunking by {chunk_by})...")
else:
typer.echo(f"Building vector index for the first time (chunking by {chunk_by})...")
try:
vector_searcher.build_index(chunk_by=chunk_by)
if format not in ["plain", "json"]:
typer.echo("Vector index built successfully")
except Exception as e:
if format == "plain":
typer.echo(f"Failed to build vector index: {e}")
else:
typer.secho(f"Failed to build vector index: {e}", fg=typer.colors.RED)
raise typer.Exit(code=1)
else:
if format not in ["plain", "json"]:
typer.echo("Using existing vector index")

# Perform semantic search
if format not in ["plain", "json"]:
typer.echo(f"Searching for: '{query}'")
try:
results = repo.search_semantic(query, top_k=top_k, embed_fn=embed_fn)
except Exception as e:
if format == "plain":
typer.echo(f"Semantic search failed: {e}")
if "collection" in str(e).lower():
typer.echo("The vector index might not exist. Try with --build-index")
else:
typer.secho(f"Semantic search failed: {e}", fg=typer.colors.RED)
# Try to provide helpful error message
if "collection" in str(e).lower():
typer.echo("The vector index might not exist. Try with --build-index")
raise typer.Exit(code=1)

# Output results
if output:
Path(output).write_text(json.dumps(results, indent=2))
if format == "plain":
typer.echo(f"Semantic search results written to {output}")
else:
typer.echo(f"Semantic search results written to {output}")
else:
if not results:
if format == "plain":
typer.echo(f"No semantic matches found for '{query}'")
typer.echo("Try building the index with --build-index or using different keywords")
else:
typer.echo(f"No semantic matches found for '{query}'")
typer.echo("Try building the index with --build-index or using different keywords")
else:
if format == "json":
typer.echo(json.dumps(results, indent=2))
elif format == "plain":
for result in results:
file_path = result.get("file", "Unknown file")
score = result.get("score", 0)
typer.echo(f"{file_path}:{score:.3f}")
else: # table format
typer.echo(f"Found {len(results)} semantic matches:")
for i, result in enumerate(results, 1):
file_path = result.get("file", "Unknown file")
name = result.get("name", "")
symbol_type = result.get("type", "")
score = result.get("score", 0)

# Format the result display
if name and symbol_type:
typer.echo(f"{i}. {file_path} - {symbol_type} '{name}' (score: {score:.3f})")
else:
typer.echo(f"{i}. {file_path} (score: {score:.3f})")

# Show a snippet of the code if available
code = result.get("code", "")
if code:
# Show first 100 characters of code, cleaned up
code_snippet = code.strip().replace("\n", " ")[:100]
if len(code_snippet) == 100:
code_snippet += "..."
typer.echo(f" {code_snippet}")
typer.echo()

except Exception as e:
typer.secho(f"❌ Error: {e}", fg=typer.colors.RED)
raise typer.Exit(code=1)


def handle_cli_error(error: Exception, error_type: str = "Error", help_text: Optional[str] = None) -> None:
"""Consistent error handling for CLI commands."""
if isinstance(error, ValueError):
Expand Down
2 changes: 1 addition & 1 deletion src/kit/docstring_indexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def default_embed_fn(text_to_embed: str) -> List[float]:
except ImportError:
raise ImportError(
"The 'sentence-transformers' library is required to use the default embedding function. "
"Please install it (e.g., 'pip install kit[default-embeddings]' or 'pip install sentence-transformers') "
"Please install it (e.g., 'pip install cased-kit[ml]' or 'pip install sentence-transformers') "
"or provide a custom 'embed_fn' to DocstringIndexer."
)
except Exception as e:
Expand Down
8 changes: 7 additions & 1 deletion src/kit/vector_searcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,13 @@ class VectorSearcher:
def __init__(self, repo, embed_fn, backend: Optional[VectorDBBackend] = None, persist_dir: Optional[str] = None):
self.repo = repo
self.embed_fn = embed_fn # Function: str -> List[float]
self.persist_dir = persist_dir or os.path.join(".kit", "vector_db")
# Make persist_dir relative to repo path if not absolute
if persist_dir is None:
self.persist_dir = os.path.join(str(self.repo.local_path), ".kit", "vector_db")
elif os.path.isabs(persist_dir):
self.persist_dir = persist_dir
else:
self.persist_dir = os.path.join(str(self.repo.local_path), persist_dir)
self.backend = backend or ChromaDBBackend(self.persist_dir)
self.chunk_metadatas: List[Dict[str, Any]] = []
self.chunk_embeddings: List[List[float]] = []
Expand Down
Loading