diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..d618786 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,28 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + +# Build documentation in the docs/ directory with MkDocs +mkdocs: + configuration: mkdocs.yml + +# Optionally build your docs in additional formats such as PDF and ePub +formats: + - pdf + - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt + - method: pip + path: . diff --git a/Makefile b/Makefile index 2376789..82cc3f8 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,10 @@ help: @echo " make upload Upload to PyPI" @echo "" @echo "Documentation:" - @echo " make docs Build documentation" + @echo " make docs Build Sphinx documentation" + @echo " make mkdocs-serve Serve MkDocs locally (port 8000)" + @echo " make mkdocs-build Build MkDocs site" + @echo " make mkdocs-deploy Deploy MkDocs with mike (versioning)" install: pip install -e . @@ -82,9 +85,18 @@ upload: build docs: cd docs && make html +mkdocs-serve: + mkdocs serve + +mkdocs-build: + mkdocs build + +mkdocs-deploy: + mike deploy --push --update-aliases $(VERSION) latest + # Shortcut aliases fmt: format l: lint t: test tc: test-cov -b: build \ No newline at end of file +b: build diff --git a/docs/READTHEDOCS_DEPLOYMENT.md b/docs/READTHEDOCS_DEPLOYMENT.md new file mode 100644 index 0000000..9dc6227 --- /dev/null +++ b/docs/READTHEDOCS_DEPLOYMENT.md @@ -0,0 +1,158 @@ +# ReadTheDocs Deployment Guide + +This guide walks you through deploying the MongoDB Query Builder documentation to ReadTheDocs.io. + +## Prerequisites + +- GitHub repository with the documentation +- ReadTheDocs account (free at readthedocs.org) +- MkDocs documentation set up (already done!) + +## Local Testing + +First, test the documentation locally: + +```bash +# Install MkDocs dependencies +pip install -r docs/requirements.txt + +# Serve documentation locally +mkdocs serve + +# Visit http://localhost:8000 to preview +``` + +## ReadTheDocs Setup + +### 1. Import Project + +1. Log in to [ReadTheDocs.org](https://readthedocs.org) +2. Click "Import a Project" +3. Select "Import from GitHub" +4. Choose your `mongodb-builder` repository +5. Click "Next" + +### 2. Project Configuration + +ReadTheDocs will automatically detect the `.readthedocs.yaml` file. Verify these settings: + +- **Name**: MongoDB Query Builder +- **Repository URL**: Your GitHub repo URL +- **Default branch**: main (or master) +- **Documentation type**: MkDocs (auto-detected) + +### 3. Advanced Settings + +In the project admin panel: + +1. **Domains**: + - Default: `mongodb-query-builder.readthedocs.io` + - Can add custom domain later + +2. **Versions**: + - Enable version tags for releases + - Set "latest" as default version + +3. **Build Settings**: + - Build on push: ✓ + - Build pull requests: ✓ (optional) + +### 4. Environment Variables (if needed) + +If you need any environment variables, add them in: +Admin → Environment Variables + +## GitHub Integration + +### Webhook Setup + +ReadTheDocs automatically sets up a webhook. Verify in your GitHub repo: + +1. Go to Settings → Webhooks +2. You should see a ReadTheDocs webhook +3. It triggers on push events + +### Build Status Badge + +Add to your README.md: + +```markdown +[![Documentation Status](https://readthedocs.org/projects/mongodb-query-builder/badge/?version=latest)](https://mongodb-query-builder.readthedocs.io/en/latest/?badge=latest) +``` + +## Version Management + +For version management with `mike`: + +```bash +# Install mike +pip install mike + +# Deploy initial version +mike deploy --push --update-aliases 0.1.0 latest + +# Deploy new version +mike deploy --push --update-aliases 0.2.0 latest + +# List versions +mike list + +# Set default version +mike set-default --push latest +``` + +## Troubleshooting + +### Build Failures + +1. Check build logs in ReadTheDocs dashboard +2. Common issues: + - Missing dependencies in `docs/requirements.txt` + - Incorrect `mkdocs.yml` syntax + - Python version mismatch + +### Custom Domain + +To use a custom domain: + +1. Add CNAME record pointing to `readthedocs.io` +2. Add domain in ReadTheDocs admin panel +3. Enable HTTPS + +### Search Not Working + +Ensure in `mkdocs.yml`: +- Search plugin is enabled +- Language is set correctly + +## Maintenance + +### Updating Documentation + +1. Make changes to documentation files +2. Commit and push to GitHub +3. ReadTheDocs automatically rebuilds + +### Monitoring + +- Check build status in ReadTheDocs dashboard +- Monitor 404 errors in analytics +- Review search queries for missing content + +## Next Steps + +1. Complete placeholder documentation files +2. Add more examples and tutorials +3. Set up documentation versioning +4. Consider adding: + - API changelog + - Contributing guide + - Architecture diagrams + - Video tutorials + +## Resources + +- [ReadTheDocs Documentation](https://docs.readthedocs.io/) +- [MkDocs Documentation](https://www.mkdocs.org/) +- [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) +- [Mike Documentation](https://github.com/jimporter/mike) diff --git a/docs/api/aggregate-builder.md b/docs/api/aggregate-builder.md new file mode 100644 index 0000000..22a72cb --- /dev/null +++ b/docs/api/aggregate-builder.md @@ -0,0 +1,615 @@ +# AggregateBuilder API Reference + +The `AggregateBuilder` class provides a fluent interface for building MongoDB aggregation pipelines. + +## Class Overview + +```python +from mongodb_query_builder import AggregateBuilder +``` + +`AggregateBuilder` allows you to construct MongoDB aggregation pipelines using a chainable API that provides type safety and intuitive method names. + +## Constructor + +```python +AggregateBuilder() +``` + +Creates a new AggregateBuilder instance with an empty pipeline. + +**Example:** +```python +pipeline_builder = AggregateBuilder() +``` + +## Core Methods + +### build() -> List[Dict[str, Any]] + +Builds and returns the final aggregation pipeline as a list of stages. + +**Returns:** +- List[Dict[str, Any]]: The MongoDB aggregation pipeline + +**Example:** +```python +pipeline = AggregateBuilder() + .match(QueryFilter().field("status").equals("active")) + .limit(10) + .build() +# Result: [{"$match": {"status": "active"}}, {"$limit": 10}] +``` + +## Pipeline Stages + +### match(filter_query: Union[QueryFilter, Dict[str, Any]]) -> AggregateBuilder + +Filters documents using a query filter. + +**Parameters:** +- `filter_query` (Union[QueryFilter, Dict[str, Any]]): Query filter or dictionary + +**Example:** +```python +# Using QueryFilter +AggregateBuilder().match( + QueryFilter().field("age").greater_than(18) +) + +# Using dictionary +AggregateBuilder().match({"age": {"$gt": 18}}) +``` + +### project(**fields: Union[int, str, Dict[str, Any]]) -> AggregateBuilder + +Reshapes documents by including, excluding, or computing new fields. + +**Parameters:** +- `**fields`: Field specifications (1 to include, 0 to exclude, or expressions) + +**Example:** +```python +# Include/exclude fields +AggregateBuilder().project( + name=1, + email=1, + _id=0 +) + +# Computed fields +AggregateBuilder().project( + fullName={"$concat": ["$firstName", " ", "$lastName"]}, + age={"$subtract": [{"$year": "$$NOW"}, {"$year": "$birthDate"}]} +) +``` + +### group(by: Optional[Union[str, Dict[str, Any]]] = None, **accumulators: Dict[str, Any]) -> AggregateBuilder + +Groups documents and applies accumulator expressions. + +**Parameters:** +- `by` (Optional[Union[str, Dict[str, Any]]]): Grouping expression or field +- `**accumulators`: Accumulator expressions + +**Example:** +```python +# Group by field +AggregateBuilder().group( + by="$category", + count={"$sum": 1}, + avgPrice={"$avg": "$price"} +) + +# Group by multiple fields +AggregateBuilder().group( + by={"category": "$category", "status": "$status"}, + total={"$sum": "$amount"} +) + +# Group all documents +AggregateBuilder().group( + by=None, + totalRevenue={"$sum": "$revenue"} +) +``` + +### sort(field: Optional[str] = None, ascending: bool = True, **fields: int) -> AggregateBuilder + +Sorts documents by one or more fields. + +**Parameters:** +- `field` (Optional[str]): Single field to sort by +- `ascending` (bool): Sort direction for single field (default: True) +- `**fields`: Multiple fields with sort direction (1 for ascending, -1 for descending) + +**Example:** +```python +# Single field sort +AggregateBuilder().sort("age", ascending=False) + +# Multiple field sort +AggregateBuilder().sort(age=-1, name=1) +``` + +### limit(count: int) -> AggregateBuilder + +Limits the number of documents in the pipeline. + +**Parameters:** +- `count` (int): Maximum number of documents + +**Example:** +```python +AggregateBuilder().limit(10) +``` + +### skip(count: int) -> AggregateBuilder + +Skips a specified number of documents. + +**Parameters:** +- `count` (int): Number of documents to skip + +**Example:** +```python +AggregateBuilder().skip(20).limit(10) # Pagination +``` + +### unwind(path: str, preserve_null_and_empty_arrays: bool = False, include_array_index: Optional[str] = None) -> AggregateBuilder + +Deconstructs an array field into multiple documents. + +**Parameters:** +- `path` (str): Array field path (with or without $) +- `preserve_null_and_empty_arrays` (bool): Keep documents without the array field +- `include_array_index` (Optional[str]): Field name for array index + +**Example:** +```python +# Simple unwind +AggregateBuilder().unwind("$tags") + +# Preserve empty arrays +AggregateBuilder().unwind( + "$items", + preserve_null_and_empty_arrays=True +) + +# Include array index +AggregateBuilder().unwind( + "$products", + include_array_index="productIndex" +) +``` + +### lookup(from_collection: str, local_field: str, foreign_field: str, as_field: str) -> AggregateBuilder + +Performs a left outer join with another collection. + +**Parameters:** +- `from_collection` (str): The collection to join +- `local_field` (str): Field from input documents +- `foreign_field` (str): Field from joined collection +- `as_field` (str): Output array field name + +**Example:** +```python +AggregateBuilder().lookup( + from_collection="users", + local_field="userId", + foreign_field="_id", + as_field="userDetails" +) +``` + +### add_fields(**fields: Dict[str, Any]) -> AggregateBuilder + +Adds new fields to documents. + +**Parameters:** +- `**fields`: Field expressions to add + +**Example:** +```python +AggregateBuilder().add_fields( + total={"$multiply": ["$price", "$quantity"]}, + discountedPrice={"$multiply": ["$price", 0.9]} +) +``` + +### set(**fields: Dict[str, Any]) -> AggregateBuilder + +Alias for add_fields (MongoDB 4.2+). + +**Parameters:** +- `**fields`: Field expressions to set + +**Example:** +```python +AggregateBuilder().set( + status="processed", + processedAt="$$NOW" +) +``` + +### unset(fields: Union[str, List[str]]) -> AggregateBuilder + +Removes fields from documents. + +**Parameters:** +- `fields` (Union[str, List[str]]): Field(s) to remove + +**Example:** +```python +# Remove single field +AggregateBuilder().unset("tempField") + +# Remove multiple fields +AggregateBuilder().unset(["temp1", "temp2", "internal"]) +``` + +### replace_root(new_root: Union[str, Dict[str, Any]]) -> AggregateBuilder + +Replaces the document with a new root document. + +**Parameters:** +- `new_root` (Union[str, Dict[str, Any]]): New root document or field path + +**Example:** +```python +# Replace with embedded document +AggregateBuilder().replace_root("$details") + +# Replace with computed document +AggregateBuilder().replace_root({ + "user": "$name", + "totalSpent": {"$sum": "$orders.amount"} +}) +``` + +### count(field_name: str = "count") -> AggregateBuilder + +Counts the number of documents and stores in a field. + +**Parameters:** +- `field_name` (str): Output field name (default: "count") + +**Example:** +```python +AggregateBuilder() + .match(QueryFilter().field("status").equals("active")) + .count("activeUsers") +``` + +### facet(**facets: Dict[str, List[Dict[str, Any]]]) -> AggregateBuilder + +Processes multiple aggregation pipelines in a single stage. + +**Parameters:** +- `**facets`: Named sub-pipelines + +**Example:** +```python +AggregateBuilder().facet( + categoryCounts=[ + {"$group": {"_id": "$category", "count": {"$sum": 1}}}, + {"$sort": {"count": -1}} + ], + priceRanges=[ + {"$bucket": { + "groupBy": "$price", + "boundaries": [0, 50, 100, 200], + "default": "Other" + }} + ], + topProducts=[ + {"$sort": {"sales": -1}}, + {"$limit": 5} + ] +) +``` + +### bucket(group_by: str, boundaries: List[Union[int, float]], default: Optional[str] = None, output: Optional[Dict[str, Any]] = None) -> AggregateBuilder + +Groups documents into buckets. + +**Parameters:** +- `group_by` (str): Expression to group by +- `boundaries` (List[Union[int, float]]): Bucket boundaries +- `default` (Optional[str]): Bucket for out-of-range values +- `output` (Optional[Dict[str, Any]]): Output specifications + +**Example:** +```python +AggregateBuilder().bucket( + group_by="$age", + boundaries=[0, 18, 30, 50, 65, 100], + default="Other", + output={ + "count": {"$sum": 1}, + "users": {"$push": "$name"} + } +) +``` + +### bucket_auto(group_by: str, buckets: int, output: Optional[Dict[str, Any]] = None) -> AggregateBuilder + +Automatically creates evenly distributed buckets. + +**Parameters:** +- `group_by` (str): Expression to group by +- `buckets` (int): Number of buckets +- `output` (Optional[Dict[str, Any]]): Output specifications + +**Example:** +```python +AggregateBuilder().bucket_auto( + group_by="$price", + buckets=5, + output={ + "count": {"$sum": 1}, + "avgPrice": {"$avg": "$price"} + } +) +``` + +### sample(size: int) -> AggregateBuilder + +Randomly selects documents. + +**Parameters:** +- `size` (int): Number of documents to sample + +**Example:** +```python +AggregateBuilder().sample(100) +``` + +### out(collection: str) -> AggregateBuilder + +Writes pipeline results to a collection. + +**Parameters:** +- `collection` (str): Output collection name + +**Example:** +```python +AggregateBuilder() + .group(by="$category", total={"$sum": "$amount"}) + .out("category_totals") +``` + +### merge(into: Union[str, Dict[str, Any]], on: Optional[Union[str, List[str]]] = None, when_matched: Optional[str] = None, when_not_matched: Optional[str] = None) -> AggregateBuilder + +Merges pipeline results into a collection. + +**Parameters:** +- `into` (Union[str, Dict[str, Any]]): Target collection +- `on` (Optional[Union[str, List[str]]]): Field(s) to match on +- `when_matched` (Optional[str]): Action for matches +- `when_not_matched` (Optional[str]): Action for non-matches + +**Example:** +```python +AggregateBuilder().merge( + into="summary_collection", + on="_id", + when_matched="merge", + when_not_matched="insert" +) +``` + +### add_stage(stage: Dict[str, Any]) -> AggregateBuilder + +Adds a custom stage to the pipeline. + +**Parameters:** +- `stage` (Dict[str, Any]): Custom stage dictionary + +**Example:** +```python +AggregateBuilder().add_stage({ + "$redact": { + "$cond": { + "if": {"$eq": ["$level", 5]}, + "then": "$$PRUNE", + "else": "$$DESCEND" + } + } +}) +``` + +## Advanced Usage + +### Complex Aggregation Pipeline + +```python +from mongodb_query_builder import AggregateBuilder, QueryFilter + +pipeline = AggregateBuilder() + # Filter active users + .match(QueryFilter().field("status").equals("active")) + + # Join with orders collection + .lookup( + from_collection="orders", + local_field="_id", + foreign_field="userId", + as_field="orders" + ) + + # Unwind orders array + .unwind("$orders") + + # Group by user and calculate totals + .group( + by="$_id", + name={"$first": "$name"}, + totalOrders={"$sum": 1}, + totalSpent={"$sum": "$orders.amount"}, + avgOrderValue={"$avg": "$orders.amount"} + ) + + # Add computed fields + .add_fields( + customerTier={ + "$switch": { + "branches": [ + {"case": {"$gte": ["$totalSpent", 10000]}, "then": "Platinum"}, + {"case": {"$gte": ["$totalSpent", 5000]}, "then": "Gold"}, + {"case": {"$gte": ["$totalSpent", 1000]}, "then": "Silver"} + ], + "default": "Bronze" + } + } + ) + + # Sort by total spent + .sort("totalSpent", ascending=False) + + # Limit to top 100 customers + .limit(100) + + # Build the pipeline + .build() +``` + +### Faceted Search Pipeline + +```python +search_pipeline = AggregateBuilder() + # Initial match + .match(QueryFilter().field("category").in_(["electronics", "computers"])) + + # Faceted aggregation + .facet( + # Main results + products=[ + {"$sort": {"popularity": -1}}, + {"$skip": 0}, + {"$limit": 20}, + {"$project": { + "name": 1, + "price": 1, + "image": 1, + "rating": 1 + }} + ], + + # Price ranges + priceRanges=[ + {"$bucket": { + "groupBy": "$price", + "boundaries": [0, 100, 500, 1000, 5000], + "default": "5000+", + "output": {"count": {"$sum": 1}} + }} + ], + + # Brand counts + brands=[ + {"$group": {"_id": "$brand", "count": {"$sum": 1}}}, + {"$sort": {"count": -1}}, + {"$limit": 10} + ], + + # Total count + totalCount=[ + {"$count": "total"} + ] + ) + .build() +``` + +### Time Series Aggregation + +```python +time_series = AggregateBuilder() + # Match date range + .match(QueryFilter() + .field("timestamp") + .between(datetime(2024, 1, 1), datetime(2024, 12, 31)) + ) + + # Group by time buckets + .group( + by={ + "year": {"$year": "$timestamp"}, + "month": {"$month": "$timestamp"}, + "day": {"$dayOfMonth": "$timestamp"} + }, + dailyRevenue={"$sum": "$amount"}, + orderCount={"$sum": 1}, + avgOrderValue={"$avg": "$amount"} + ) + + # Calculate running totals + .set( + date={ + "$dateFromParts": { + "year": "$_id.year", + "month": "$_id.month", + "day": "$_id.day" + } + } + ) + + # Sort by date + .sort("date", ascending=True) + + # Add running total + .set( + runningTotal={ + "$sum": { + "$slice": ["$dailyRevenue", {"$add": ["$$CURRENT.index", 1]}] + } + } + ) + .build() +``` + +## Performance Considerations + +1. **Stage Order Matters**: Place `$match` stages early to reduce documents +2. **Index Usage**: Ensure `$match` and `$sort` can use indexes +3. **Memory Limits**: Aggregations have a 100MB memory limit per stage +4. **Allow Disk Use**: For large datasets, enable disk usage +5. **Pipeline Optimization**: MongoDB optimizes certain stage sequences + +## Error Handling + +AggregateBuilder raises `AggregateBuilderError` for invalid operations: + +```python +from mongodb_query_builder import AggregateBuilder, AggregateBuilderError + +try: + pipeline = AggregateBuilder() + .limit(-1) # Invalid limit value + .build() +except AggregateBuilderError as e: + print(f"Pipeline construction error: {e}") +``` + +## Integration with QueryFilter + +```python +# Create reusable filters +active_filter = QueryFilter().field("status").equals("active") +recent_filter = QueryFilter().field("created").greater_than(datetime.now() - timedelta(days=30)) + +# Use in pipeline +pipeline = AggregateBuilder() + .match(active_filter) + .match(recent_filter) + .group(by="$category", count={"$sum": 1}) + .build() +``` + +## See Also + +- [QueryFilter](query-filter.md) - For building match conditions +- [AtlasSearchBuilder](atlas-search-builder.md) - For Atlas Search integration +- [Aggregation Tutorial](../tutorials/02-aggregation-pipelines.md) - Step-by-step guide +- [Performance Guide](../performance-guide.md) - Optimization tips diff --git a/docs/api/atlas-search-builder.md b/docs/api/atlas-search-builder.md new file mode 100644 index 0000000..a646d90 --- /dev/null +++ b/docs/api/atlas-search-builder.md @@ -0,0 +1,547 @@ +# AtlasSearchBuilder API Reference + +The `AtlasSearchBuilder` class provides a fluent interface for building MongoDB Atlas Search queries. + +## Class Overview + +```python +from mongodb_query_builder import AtlasSearchBuilder +``` + +`AtlasSearchBuilder` allows you to construct Atlas Search queries with support for text search, compound queries, facets, and advanced search features. + +## Constructor + +```python +AtlasSearchBuilder(index: str = "default") +``` + +Creates a new AtlasSearchBuilder instance. + +**Parameters:** +- `index` (str): The search index name (default: "default") + +**Example:** +```python +search = AtlasSearchBuilder() +search_custom = AtlasSearchBuilder(index="custom_index") +``` + +## Core Methods + +### build() -> Dict[str, Any] + +Builds and returns the final search query dictionary. + +**Returns:** +- Dict[str, Any]: The search query configuration + +**Example:** +```python +search_query = AtlasSearchBuilder() + .text("python developer", path="description") + .build() +# Result: {"text": {"query": "python developer", "path": "description"}} +``` + +### build_stage() -> Dict[str, Any] + +Builds and returns a complete $search aggregation stage. + +**Returns:** +- Dict[str, Any]: The $search stage for aggregation pipeline + +**Example:** +```python +search_stage = AtlasSearchBuilder() + .text("mongodb", path="title") + .build_stage() +# Result: {"$search": {"index": "default", "text": {"query": "mongodb", "path": "title"}}} +``` + +## Text Search Methods + +### text(query: str, path: Union[str, List[str]], fuzzy: Optional[Dict[str, Any]] = None, score: Optional[Dict[str, Any]] = None) -> AtlasSearchBuilder + +Performs text search on specified fields. + +**Parameters:** +- `query` (str): The search query string +- `path` (Union[str, List[str]]): Field(s) to search +- `fuzzy` (Optional[Dict[str, Any]]): Fuzzy matching options +- `score` (Optional[Dict[str, Any]]): Scoring options + +**Example:** +```python +# Simple text search +AtlasSearchBuilder().text("python", path="skills") + +# Search multiple fields +AtlasSearchBuilder().text( + "full stack developer", + path=["title", "description", "requirements"] +) + +# Fuzzy search +AtlasSearchBuilder().text( + "pythn", # Misspelled + path="skills", + fuzzy={"maxEdits": 2, "prefixLength": 2} +) + +# Custom scoring +AtlasSearchBuilder().text( + "senior", + path="level", + score={"boost": {"value": 2.0}} +) +``` + +### phrase(query: str, path: Union[str, List[str]], slop: Optional[int] = None, score: Optional[Dict[str, Any]] = None) -> AtlasSearchBuilder + +Searches for exact phrase matches. + +**Parameters:** +- `query` (str): The exact phrase to search +- `path` (Union[str, List[str]]): Field(s) to search +- `slop` (Optional[int]): Maximum word distance for matches +- `score` (Optional[Dict[str, Any]]): Scoring options + +**Example:** +```python +# Exact phrase +AtlasSearchBuilder().phrase("machine learning", path="skills") + +# Phrase with slop +AtlasSearchBuilder().phrase( + "python developer", + path="title", + slop=2 # Allows up to 2 words between "python" and "developer" +) +``` + +### wildcard(query: str, path: Union[str, List[str]], allow_analyzed_field: bool = False) -> AtlasSearchBuilder + +Performs wildcard pattern matching. + +**Parameters:** +- `query` (str): Wildcard pattern (* for multiple chars, ? for single char) +- `path` (Union[str, List[str]]): Field(s) to search +- `allow_analyzed_field` (bool): Allow search on analyzed fields + +**Example:** +```python +# Wildcard search +AtlasSearchBuilder().wildcard("py*", path="language") +AtlasSearchBuilder().wildcard("user?", path="username") +``` + +### regex(pattern: str, path: Union[str, List[str]], allow_analyzed_field: bool = False) -> AtlasSearchBuilder + +Performs regular expression matching. + +**Parameters:** +- `pattern` (str): Regular expression pattern +- `path` (Union[str, List[str]]): Field(s) to search +- `allow_analyzed_field` (bool): Allow search on analyzed fields + +**Example:** +```python +# Regex search +AtlasSearchBuilder().regex( + "^[A-Z]{2,4}-\\d{4}$", + path="product_code" +) +``` + +### autocomplete(query: str, path: str, token_order: str = "any", fuzzy: Optional[Dict[str, Any]] = None) -> AtlasSearchBuilder + +Provides autocomplete/type-ahead functionality. + +**Parameters:** +- `query` (str): Partial query string +- `path` (str): Field configured for autocomplete +- `token_order` (str): "any" or "sequential" +- `fuzzy` (Optional[Dict[str, Any]]): Fuzzy matching options + +**Example:** +```python +# Autocomplete search +AtlasSearchBuilder().autocomplete( + "pyth", + path="title", + token_order="sequential" +) + +# Fuzzy autocomplete +AtlasSearchBuilder().autocomplete( + "mong", + path="technology", + fuzzy={"maxEdits": 1} +) +``` + +## Compound Queries + +### compound(must: Optional[List[Dict[str, Any]]] = None, must_not: Optional[List[Dict[str, Any]]] = None, should: Optional[List[Dict[str, Any]]] = None, filter: Optional[List[Dict[str, Any]]] = None) -> AtlasSearchBuilder + +Creates a compound query with multiple clauses. + +**Parameters:** +- `must` (Optional[List[Dict[str, Any]]]): Required matches +- `must_not` (Optional[List[Dict[str, Any]]]): Excluded matches +- `should` (Optional[List[Dict[str, Any]]]): Optional matches (affects score) +- `filter` (Optional[List[Dict[str, Any]]]): Required matches (no score effect) + +**Example:** +```python +from mongodb_query_builder import AtlasSearchBuilder, CompoundBuilder + +# Using compound method directly +search = AtlasSearchBuilder().compound( + must=[ + {"text": {"query": "python", "path": "skills"}} + ], + should=[ + {"text": {"query": "senior", "path": "level"}} + ], + filter=[ + {"range": {"path": "experience", "gte": 3}} + ] +) + +# Using CompoundBuilder (recommended) +compound = CompoundBuilder() +compound.must().text("python", path="skills") +compound.should().text("senior", path="level") +compound.filter().range("experience", gte=3) + +search = AtlasSearchBuilder().compound(compound) +``` + +## CompoundBuilder Class + +The `CompoundBuilder` class provides a fluent interface for building compound queries. + +### Constructor + +```python +CompoundBuilder() +``` + +### Methods + +#### must() -> CompoundClauseBuilder + +Returns a builder for required clauses. + +```python +compound = CompoundBuilder() +compound.must().text("required term", path="field") +``` + +#### must_not() -> CompoundClauseBuilder + +Returns a builder for excluded clauses. + +```python +compound.must_not().text("excluded term", path="field") +``` + +#### should() -> CompoundClauseBuilder + +Returns a builder for optional clauses that affect scoring. + +```python +compound.should().text("bonus term", path="field", score=2.0) +``` + +#### filter() -> CompoundClauseBuilder + +Returns a builder for required clauses that don't affect scoring. + +```python +compound.filter().range("date", gte=datetime(2024, 1, 1)) +``` + +### CompoundClauseBuilder Methods + +All clause builders support the same search methods as AtlasSearchBuilder: + +- `text(query, path, fuzzy=None, score=None)` +- `phrase(query, path, slop=None, score=None)` +- `wildcard(query, path, allow_analyzed_field=False)` +- `regex(pattern, path, allow_analyzed_field=False)` +- `range(path, gt=None, gte=None, lt=None, lte=None)` +- `equals(path, value)` +- `exists(path, value=True)` + +## Range and Comparison Methods + +### range(path: str, gt: Optional[Any] = None, gte: Optional[Any] = None, lt: Optional[Any] = None, lte: Optional[Any] = None) -> AtlasSearchBuilder + +Searches for documents within a range. + +**Parameters:** +- `path` (str): Field path +- `gt` (Optional[Any]): Greater than value +- `gte` (Optional[Any]): Greater than or equal value +- `lt` (Optional[Any]): Less than value +- `lte` (Optional[Any]): Less than or equal value + +**Example:** +```python +# Numeric range +AtlasSearchBuilder().range("price", gte=100, lte=500) + +# Date range +AtlasSearchBuilder().range( + "created_date", + gte=datetime(2024, 1, 1), + lt=datetime(2024, 7, 1) +) +``` + +### equals(path: str, value: Any) -> AtlasSearchBuilder + +Searches for exact value matches. + +**Parameters:** +- `path` (str): Field path +- `value` (Any): Value to match + +**Example:** +```python +AtlasSearchBuilder().equals("status", "active") +AtlasSearchBuilder().equals("category_id", 42) +``` + +### exists(path: str, value: bool = True) -> AtlasSearchBuilder + +Searches for documents with or without a field. + +**Parameters:** +- `path` (str): Field path +- `value` (bool): True to find documents with field, False for without + +**Example:** +```python +AtlasSearchBuilder().exists("premium_features") +AtlasSearchBuilder().exists("deleted_at", value=False) +``` + +## Faceting + +### facet(name: str, type: str, path: str, num_buckets: Optional[int] = None, boundaries: Optional[List[Union[int, float]]] = None) -> AtlasSearchBuilder + +Adds faceted search for categorization and filtering. + +**Parameters:** +- `name` (str): Facet name +- `type` (str): Facet type ("string", "number", "date") +- `path` (str): Field path +- `num_buckets` (Optional[int]): Number of buckets for numeric facets +- `boundaries` (Optional[List]): Custom boundaries for buckets + +**Example:** +```python +# String facet +search = AtlasSearchBuilder() + .text("laptop", path="name") + .facet("brands", type="string", path="brand") + .facet("categories", type="string", path="category") + +# Numeric facet with auto buckets +search.facet("price_ranges", type="number", path="price", num_buckets=5) + +# Numeric facet with custom boundaries +search.facet( + "price_ranges", + type="number", + path="price", + boundaries=[0, 100, 500, 1000, 5000] +) +``` + +## Search Options + +### highlight(path: Union[str, List[str]], max_chars_to_examine: Optional[int] = None, max_num_passages: Optional[int] = None) -> AtlasSearchBuilder + +Adds highlighting for matched terms. + +**Parameters:** +- `path` (Union[str, List[str]]): Fields to highlight +- `max_chars_to_examine` (Optional[int]): Max characters to examine +- `max_num_passages` (Optional[int]): Max highlighted passages + +**Example:** +```python +AtlasSearchBuilder() + .text("python mongodb", path=["title", "description"]) + .highlight( + path=["title", "description"], + max_num_passages=3 + ) +``` + +### count_documents(threshold: Optional[int] = None) -> AtlasSearchBuilder + +Configures document counting behavior. + +**Parameters:** +- `threshold` (Optional[int]): Counting threshold + +**Example:** +```python +AtlasSearchBuilder() + .text("query", path="field") + .count_documents(threshold=1000) +``` + +### return_stored_source(value: bool = True) -> AtlasSearchBuilder + +Controls whether to return stored source fields. + +**Parameters:** +- `value` (bool): Whether to return stored source + +**Example:** +```python +AtlasSearchBuilder() + .text("query", path="field") + .return_stored_source(True) +``` + +## Advanced Examples + +### Complex E-commerce Search + +```python +from mongodb_query_builder import AtlasSearchBuilder, CompoundBuilder + +# Create compound query +compound = CompoundBuilder() + +# Must have search term in name or description +compound.must().text( + "wireless headphones", + path=["name", "description"] +) + +# Should prefer highly rated items +compound.should().range("rating", gte=4.0) + +# Filter by price range +compound.filter().range("price", gte=50, lte=300) + +# Filter by availability +compound.filter().equals("in_stock", True) + +# Build search with facets +search = AtlasSearchBuilder() + .compound(compound) + .facet("brands", type="string", path="brand") + .facet("price_ranges", type="number", path="price", + boundaries=[0, 50, 100, 200, 500, 1000]) + .facet("ratings", type="number", path="rating", + boundaries=[0, 2, 3, 4, 4.5, 5]) + .highlight(["name", "description"]) + .build_stage() +``` + +### Multi-language Search + +```python +# Search with language-specific analyzers +search = AtlasSearchBuilder(index="multilingual_index") + .compound( + should=[ + {"text": {"query": "ordinateur", "path": "title_fr"}}, + {"text": {"query": "computer", "path": "title_en"}}, + {"text": {"query": "コンピューター", "path": "title_ja"}} + ] + ) + .build_stage() +``` + +### Geo-aware Search + +```python +# Search near a location +compound = CompoundBuilder() +compound.must().text("restaurant", path="type") +compound.filter().near( + path="location", + origin={"type": "Point", "coordinates": [-73.98, 40.75]}, + pivot=1000, # 1km + score={"boost": {"value": 2.0}} +) + +search = AtlasSearchBuilder().compound(compound).build_stage() +``` + +### Search with Synonyms + +```python +# Using synonym mapping +search = AtlasSearchBuilder(index="synonym_index") + .text( + "laptop", # Will also match "notebook", "portable computer" + path="product_name", + synonyms="technology_synonyms" + ) + .build_stage() +``` + +## Performance Tips + +1. **Use Compound Queries**: Combine filters to reduce result sets early +2. **Index Configuration**: Ensure Atlas Search indexes are properly configured +3. **Field Selection**: Search only necessary fields +4. **Scoring Strategy**: Use `filter` clauses for non-scoring criteria +5. **Facet Optimization**: Limit facets to necessary fields + +## Error Handling + +AtlasSearchBuilder raises `AtlasSearchBuilderError` for invalid operations: + +```python +from mongodb_query_builder import AtlasSearchBuilder, AtlasSearchBuilderError + +try: + search = AtlasSearchBuilder() + .text("") # Empty query + .build() +except AtlasSearchBuilderError as e: + print(f"Search construction error: {e}") +``` + +## Integration with Aggregation Pipeline + +```python +from mongodb_query_builder import AggregateBuilder, AtlasSearchBuilder + +# Create search stage +search_stage = AtlasSearchBuilder() + .text("python developer", path=["title", "skills"]) + .build_stage() + +# Use in aggregation pipeline +pipeline = AggregateBuilder() + .add_stage(search_stage) + .project( + title=1, + skills=1, + score={"$meta": "searchScore"} + ) + .sort("score", ascending=False) + .limit(10) + .build() +``` + +## See Also + +- [QueryFilter](query-filter.md) - For standard MongoDB queries +- [AggregateBuilder](aggregate-builder.md) - For aggregation pipelines +- [Atlas Search Tutorial](../tutorials/03-atlas-search.md) - Step-by-step guide +- [MongoDB Atlas Search Docs](https://docs.atlas.mongodb.com/atlas-search/) diff --git a/docs/api/query-filter.md b/docs/api/query-filter.md new file mode 100644 index 0000000..0e11b8a --- /dev/null +++ b/docs/api/query-filter.md @@ -0,0 +1,492 @@ +# QueryFilter API Reference + +The `QueryFilter` class provides a fluent interface for building MongoDB query filters. + +## Class Overview + +```python +from mongodb_query_builder import QueryFilter +``` + +`QueryFilter` allows you to construct MongoDB queries using a chainable API that provides type safety and validation. + +## Constructor + +```python +QueryFilter() +``` + +Creates a new QueryFilter instance with an empty query. + +**Example:** +```python +query = QueryFilter() +``` + +## Core Methods + +### field(name: str) -> QueryFilter + +Sets the current field for subsequent operations. + +**Parameters:** +- `name` (str): The field name to query + +**Returns:** +- QueryFilter: Returns self for method chaining + +**Example:** +```python +query = QueryFilter().field("age").greater_than(18) +``` + +### build() -> Dict[str, Any] + +Builds and returns the final MongoDB query dictionary. + +**Returns:** +- Dict[str, Any]: The MongoDB query document + +**Example:** +```python +query_dict = QueryFilter().field("status").equals("active").build() +# Result: {"status": "active"} +``` + +## Comparison Operators + +### equals(value: Any) -> QueryFilter + +Matches documents where the field equals the specified value. + +**Parameters:** +- `value` (Any): The value to match + +**Example:** +```python +QueryFilter().field("status").equals("active") +# Result: {"status": "active"} +``` + +### not_equals(value: Any) -> QueryFilter + +Matches documents where the field does not equal the specified value. + +**Parameters:** +- `value` (Any): The value to not match + +**Example:** +```python +QueryFilter().field("status").not_equals("deleted") +# Result: {"status": {"$ne": "deleted"}} +``` + +### greater_than(value: Union[int, float, datetime]) -> QueryFilter + +Matches documents where the field is greater than the specified value. + +**Parameters:** +- `value` (Union[int, float, datetime]): The value to compare against + +**Example:** +```python +QueryFilter().field("age").greater_than(18) +# Result: {"age": {"$gt": 18}} +``` + +### greater_than_or_equal(value: Union[int, float, datetime]) -> QueryFilter + +Matches documents where the field is greater than or equal to the specified value. + +**Parameters:** +- `value` (Union[int, float, datetime]): The value to compare against + +**Example:** +```python +QueryFilter().field("age").greater_than_or_equal(18) +# Result: {"age": {"$gte": 18}} +``` + +### less_than(value: Union[int, float, datetime]) -> QueryFilter + +Matches documents where the field is less than the specified value. + +**Parameters:** +- `value` (Union[int, float, datetime]): The value to compare against + +**Example:** +```python +QueryFilter().field("price").less_than(100) +# Result: {"price": {"$lt": 100}} +``` + +### less_than_or_equal(value: Union[int, float, datetime]) -> QueryFilter + +Matches documents where the field is less than or equal to the specified value. + +**Parameters:** +- `value` (Union[int, float, datetime]): The value to compare against + +**Example:** +```python +QueryFilter().field("price").less_than_or_equal(100) +# Result: {"price": {"$lte": 100}} +``` + +### between(start: Union[int, float, datetime], end: Union[int, float, datetime]) -> QueryFilter + +Matches documents where the field value is between start and end (inclusive). + +**Parameters:** +- `start` (Union[int, float, datetime]): The minimum value (inclusive) +- `end` (Union[int, float, datetime]): The maximum value (inclusive) + +**Example:** +```python +QueryFilter().field("age").between(18, 65) +# Result: {"age": {"$gte": 18, "$lte": 65}} +``` + +### in_(values: List[Any]) -> QueryFilter + +Matches documents where the field value is in the specified list. + +**Parameters:** +- `values` (List[Any]): List of values to match + +**Example:** +```python +QueryFilter().field("status").in_(["active", "pending"]) +# Result: {"status": {"$in": ["active", "pending"]}} +``` + +### not_in(values: List[Any]) -> QueryFilter + +Matches documents where the field value is not in the specified list. + +**Parameters:** +- `values` (List[Any]): List of values to not match + +**Example:** +```python +QueryFilter().field("status").not_in(["deleted", "archived"]) +# Result: {"status": {"$nin": ["deleted", "archived"]}} +``` + +## Logical Operators + +### all_of(conditions: List[QueryFilter]) -> QueryFilter + +Combines multiple conditions with AND logic. + +**Parameters:** +- `conditions` (List[QueryFilter]): List of QueryFilter conditions + +**Example:** +```python +QueryFilter().all_of([ + QueryFilter().field("age").greater_than(18), + QueryFilter().field("status").equals("active") +]) +# Result: {"$and": [{"age": {"$gt": 18}}, {"status": "active"}]} +``` + +### any_of(conditions: List[QueryFilter]) -> QueryFilter + +Combines multiple conditions with OR logic. + +**Parameters:** +- `conditions` (List[QueryFilter]): List of QueryFilter conditions + +**Example:** +```python +QueryFilter().any_of([ + QueryFilter().field("role").equals("admin"), + QueryFilter().field("role").equals("moderator") +]) +# Result: {"$or": [{"role": "admin"}, {"role": "moderator"}]} +``` + +### none_of(conditions: List[QueryFilter]) -> QueryFilter + +Combines multiple conditions with NOR logic. + +**Parameters:** +- `conditions` (List[QueryFilter]): List of QueryFilter conditions + +**Example:** +```python +QueryFilter().none_of([ + QueryFilter().field("status").equals("deleted"), + QueryFilter().field("status").equals("archived") +]) +# Result: {"$nor": [{"status": "deleted"}, {"status": "archived"}]} +``` + +### not_(condition: QueryFilter) -> QueryFilter + +Negates a condition. + +**Parameters:** +- `condition` (QueryFilter): The condition to negate + +**Example:** +```python +QueryFilter().not_( + QueryFilter().field("status").equals("active") +) +# Result: {"$not": {"status": "active"}} +``` + +## Array Operators + +### array_contains(value: Any) -> QueryFilter + +Matches arrays that contain the specified value. + +**Parameters:** +- `value` (Any): The value to check for + +**Example:** +```python +QueryFilter().field("tags").array_contains("python") +# Result: {"tags": "python"} +``` + +### array_contains_all(values: List[Any]) -> QueryFilter + +Matches arrays that contain all specified values. + +**Parameters:** +- `values` (List[Any]): List of values that must all be present + +**Example:** +```python +QueryFilter().field("skills").array_contains_all(["python", "mongodb"]) +# Result: {"skills": {"$all": ["python", "mongodb"]}} +``` + +### array_size(size: int) -> QueryFilter + +Matches arrays with the specified size. + +**Parameters:** +- `size` (int): The exact array size + +**Example:** +```python +QueryFilter().field("items").array_size(3) +# Result: {"items": {"$size": 3}} +``` + +### array_element_match(condition: Dict[str, Any]) -> QueryFilter + +Matches arrays where at least one element matches the condition. + +**Parameters:** +- `condition` (Dict[str, Any]): The condition for array elements + +**Example:** +```python +QueryFilter().field("scores").array_element_match({"$gt": 80}) +# Result: {"scores": {"$elemMatch": {"$gt": 80}}} +``` + +## String Operators + +### starts_with(prefix: str, case_sensitive: bool = True) -> QueryFilter + +Matches strings that start with the specified prefix. + +**Parameters:** +- `prefix` (str): The prefix to match +- `case_sensitive` (bool): Whether the match is case-sensitive (default: True) + +**Example:** +```python +QueryFilter().field("name").starts_with("John") +# Result: {"name": {"$regex": "^John"}} + +QueryFilter().field("name").starts_with("john", case_sensitive=False) +# Result: {"name": {"$regex": "^john", "$options": "i"}} +``` + +### ends_with(suffix: str, case_sensitive: bool = True) -> QueryFilter + +Matches strings that end with the specified suffix. + +**Parameters:** +- `suffix` (str): The suffix to match +- `case_sensitive` (bool): Whether the match is case-sensitive (default: True) + +**Example:** +```python +QueryFilter().field("email").ends_with("@example.com") +# Result: {"email": {"$regex": "@example\\.com$"}} +``` + +### contains(substring: str, case_sensitive: bool = True) -> QueryFilter + +Matches strings that contain the specified substring. + +**Parameters:** +- `substring` (str): The substring to match +- `case_sensitive` (bool): Whether the match is case-sensitive (default: True) + +**Example:** +```python +QueryFilter().field("description").contains("python") +# Result: {"description": {"$regex": "python"}} +``` + +### regex(pattern: str, options: Optional[str] = None) -> QueryFilter + +Matches strings using a regular expression. + +**Parameters:** +- `pattern` (str): The regular expression pattern +- `options` (Optional[str]): MongoDB regex options (e.g., "i" for case-insensitive) + +**Example:** +```python +QueryFilter().field("email").regex(r".*@example\.com$", "i") +# Result: {"email": {"$regex": ".*@example\\.com$", "$options": "i"}} +``` + +## Element Operators + +### exists(value: bool = True) -> QueryFilter + +Matches documents that have (or don't have) the specified field. + +**Parameters:** +- `value` (bool): True to match documents with the field, False for without + +**Example:** +```python +QueryFilter().field("email").exists() +# Result: {"email": {"$exists": true}} + +QueryFilter().field("deleted_at").exists(False) +# Result: {"deleted_at": {"$exists": false}} +``` + +### type_check(bson_type: Union[str, int]) -> QueryFilter + +Matches documents where the field is of the specified BSON type. + +**Parameters:** +- `bson_type` (Union[str, int]): The BSON type name or number + +**Example:** +```python +QueryFilter().field("age").type_check("int") +# Result: {"age": {"$type": "int"}} + +QueryFilter().field("tags").type_check("array") +# Result: {"tags": {"$type": "array"}} +``` + +## Geo Operators + +### near(coordinates: List[float], max_distance: Optional[float] = None, min_distance: Optional[float] = None) -> QueryFilter + +Finds documents near a geographic point. + +**Parameters:** +- `coordinates` (List[float]): [longitude, latitude] coordinates +- `max_distance` (Optional[float]): Maximum distance in meters +- `min_distance` (Optional[float]): Minimum distance in meters + +**Example:** +```python +QueryFilter().field("location").near([40.7128, -74.0060], max_distance=1000) +# Result: {"location": {"$near": {"$geometry": {"type": "Point", "coordinates": [40.7128, -74.0060]}, "$maxDistance": 1000}}} +``` + +### within_box(bottom_left: List[float], top_right: List[float]) -> QueryFilter + +Finds documents within a rectangular area. + +**Parameters:** +- `bottom_left` (List[float]): [longitude, latitude] of bottom-left corner +- `top_right` (List[float]): [longitude, latitude] of top-right corner + +**Example:** +```python +QueryFilter().field("location").within_box([40.0, -75.0], [41.0, -73.0]) +# Result: {"location": {"$geoWithin": {"$box": [[40.0, -75.0], [41.0, -73.0]]}}} +``` + +## Advanced Usage + +### Combining Multiple Fields + +```python +query = QueryFilter() + .field("age").greater_than(18) + .field("status").equals("active") + .field("role").in_(["user", "admin"]) + .build() +# Result: {"age": {"$gt": 18}, "status": "active", "role": {"$in": ["user", "admin"]}} +``` + +### Nested Field Queries + +```python +query = QueryFilter() + .field("address.city").equals("New York") + .field("address.zipcode").starts_with("100") + .build() +# Result: {"address.city": "New York", "address.zipcode": {"$regex": "^100"}} +``` + +### Complex Logical Combinations + +```python +query = QueryFilter().all_of([ + QueryFilter().field("type").equals("product"), + QueryFilter().any_of([ + QueryFilter().field("price").less_than(50), + QueryFilter().all_of([ + QueryFilter().field("price").between(50, 100), + QueryFilter().field("discount").greater_than(0.2) + ]) + ]) +]).build() +``` + +## Error Handling + +QueryFilter raises `QueryFilterError` for invalid operations: + +```python +from mongodb_query_builder import QueryFilter, QueryFilterError + +try: + # This will raise an error - no field specified + query = QueryFilter().equals("value").build() +except QueryFilterError as e: + print(f"Error: {e}") +``` + +Common error scenarios: +- Using operators without calling `field()` first +- Passing invalid types to operators +- Empty arrays for `in_()` or `array_contains_all()` +- Invalid regex patterns + +## Performance Tips + +1. **Use indexes**: Design queries to take advantage of MongoDB indexes +2. **Limit fields**: Query only the fields you need +3. **Use type-appropriate operators**: Use numeric operators for numbers, string operators for strings +4. **Avoid complex regex**: Anchored regex (^prefix) performs better than contains patterns +5. **Combine conditions efficiently**: Use compound indexes for multi-field queries + +## See Also + +- [AggregateBuilder](aggregate-builder.md) - For aggregation pipelines +- [AtlasSearchBuilder](atlas-search-builder.md) - For Atlas Search queries +- [Migration Guide](../migration-guide.md) - Converting from raw MongoDB queries +- [Performance Guide](../performance-guide.md) - Query optimization tips diff --git a/docs/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css new file mode 100644 index 0000000..65cd01f --- /dev/null +++ b/docs/assets/stylesheets/extra.css @@ -0,0 +1,78 @@ +/* Extra CSS for MongoDB Query Builder Documentation */ + +/* Custom color scheme */ +:root { + --md-primary-fg-color: #00684a; + --md-primary-fg-color--light: #00a074; + --md-primary-fg-color--dark: #00452e; + --md-accent-fg-color: #13aa52; +} + +/* MongoDB brand colors for dark mode */ +[data-md-color-scheme="slate"] { + --md-primary-fg-color: #13aa52; + --md-primary-fg-color--light: #21c366; + --md-primary-fg-color--dark: #00684a; + --md-accent-fg-color: #00ed64; +} + +/* Code block styling */ +.highlight pre { + border-radius: 8px; +} + +/* Admonition styling */ +.md-typeset .admonition, +.md-typeset details { + border-radius: 8px; +} + +/* Table styling */ +.md-typeset table:not([class]) { + border-radius: 8px; + overflow: hidden; +} + +/* Navigation enhancement */ +.md-nav__link--active { + font-weight: 600; +} + +/* Search result highlighting */ +.md-search-result mark { + background-color: var(--md-accent-fg-color); + color: var(--md-default-bg-color); +} + +/* Version selector styling */ +.md-version__current { + font-weight: 600; +} + +/* API reference styling */ +.doc-class > header { + border-left: 4px solid var(--md-primary-fg-color); + padding-left: 1rem; +} + +.doc-function > header { + border-left: 4px solid var(--md-accent-fg-color); + padding-left: 1rem; +} + +/* Mermaid diagram styling */ +.mermaid { + text-align: center; +} + +/* Footer styling */ +.md-footer-meta { + background-color: var(--md-default-fg-color--lightest); +} + +/* Mobile navigation */ +@media screen and (max-width: 76.1875em) { + .md-nav--primary .md-nav__title { + background-color: var(--md-primary-fg-color); + } +} diff --git a/docs/cookbook/analytics.md b/docs/cookbook/analytics.md new file mode 100644 index 0000000..e8267ca --- /dev/null +++ b/docs/cookbook/analytics.md @@ -0,0 +1,3 @@ +# Analytics Pipelines + +This cookbook recipe is coming soon! \ No newline at end of file diff --git a/docs/cookbook/authentication.md b/docs/cookbook/authentication.md new file mode 100644 index 0000000..b1161cb --- /dev/null +++ b/docs/cookbook/authentication.md @@ -0,0 +1,3 @@ +# Authentication Patterns + +This cookbook recipe is coming soon! \ No newline at end of file diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md new file mode 100644 index 0000000..8a6fbb3 --- /dev/null +++ b/docs/cookbook/index.md @@ -0,0 +1,95 @@ +# MongoDB Query Builder Cookbook + +Welcome to the MongoDB Query Builder Cookbook! This section contains practical, real-world examples and patterns for common use cases. + +## Available Recipes + +### 🔐 [Authentication Patterns](authentication.md) +Learn how to implement secure user authentication patterns with MongoDB Query Builder, including: +- User registration and login +- Password hashing and verification +- Session management +- Role-based access control + +### 🛍️ [Product Catalog Search](product-catalog.md) +Build powerful e-commerce search functionality: +- Full-text search with Atlas Search +- Faceted search and filtering +- Product recommendations +- Inventory management queries + +### 📊 [Analytics Pipelines](analytics.md) +Create sophisticated analytics pipelines: +- Sales analytics and reporting +- User behavior analysis +- Real-time dashboards +- Data aggregation patterns + +### ⏰ [Time Series Data](time-series.md) +Handle time-series data effectively: +- IoT sensor data processing +- Financial data analysis +- Log aggregation +- Performance metrics + +## How to Use This Cookbook + +Each recipe in this cookbook includes: + +1. **Problem Statement** - What we're trying to solve +2. **Solution Overview** - High-level approach +3. **Implementation** - Complete code examples +4. **Explanation** - Detailed walkthrough +5. **Variations** - Alternative approaches +6. **Performance Tips** - Optimization suggestions + +## Quick Example + +Here's a taste of what you'll find in the cookbook: + +```python +from mongodb_query_builder import AggregateBuilder + +# Calculate daily sales with running totals +pipeline = ( + AggregateBuilder() + .match({"status": "completed"}) + .group({ + "_id": { + "year": {"$year": "$created_at"}, + "month": {"$month": "$created_at"}, + "day": {"$dayOfMonth": "$created_at"} + }, + "daily_sales": {"$sum": "$total"}, + "order_count": {"$sum": 1} + }) + .sort({"_id.year": 1, "_id.month": 1, "_id.day": 1}) + .set_window_fields( + sort_by="_id", + output={ + "running_total": { + "$sum": "$daily_sales", + "window": { + "documents": ["unbounded", "current"] + } + } + } + ) + .build() +) +``` + +## Contributing Recipes + +Have a great pattern or solution? We'd love to include it! Please: + +1. Follow the recipe format used in existing examples +2. Include complete, working code +3. Add performance considerations +4. Submit a pull request + +## Need Help? + +- Check the [API Reference](../api/query-filter.md) for detailed method documentation +- Read the [User Guide](../tutorials/01-basic-queries.md) for comprehensive tutorials +- Join our [community discussions](https://github.com/yourusername/mongodb-builder/discussions) diff --git a/docs/cookbook/product-catalog.md b/docs/cookbook/product-catalog.md new file mode 100644 index 0000000..d69c1a4 --- /dev/null +++ b/docs/cookbook/product-catalog.md @@ -0,0 +1,3 @@ +# Product Catalog Search + +This cookbook recipe is coming soon! \ No newline at end of file diff --git a/docs/cookbook/time-series.md b/docs/cookbook/time-series.md new file mode 100644 index 0000000..a702aaa --- /dev/null +++ b/docs/cookbook/time-series.md @@ -0,0 +1,3 @@ +# Time Series Data + +This cookbook recipe is coming soon! \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..129a756 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,329 @@ +# Getting Started with MongoDB Query Builder + +This guide will help you get up and running with MongoDB Query Builder in just a few minutes. + +## Installation + +### Basic Installation + +Install MongoDB Query Builder using pip: + +```bash +pip install mongodb-query-builder +``` + +### Installation with MongoDB Support + +If you want to use the library with pymongo: + +```bash +pip install mongodb-query-builder[mongodb] +``` + +### Development Installation + +For development or to run tests: + +```bash +git clone https://github.com/ch-dev401/mongodb-query-builder.git +cd mongodb-query-builder +pip install -e ".[dev,test]" +``` + +### Install from Source as Package + +To install the library from source as a regular package (not in development mode): + +```bash +# Clone the repository +git clone https://github.com/ch-dev401/mongodb-query-builder.git +cd mongodb-query-builder + +# Install as a package +pip install . + +# Or with MongoDB support +pip install ".[mongodb]" + +# Or build and install using build tools +python -m build +pip install dist/mongodb_query_builder-*.whl +``` + +### Install from GitHub + +You can also install directly from GitHub: + +```bash +# Install latest from main branch +pip install git+https://github.com/ch-dev401/mongodb-query-builder.git + +# Install a specific version/tag +pip install git+https://github.com/ch-dev401/mongodb-query-builder.git@v1.0.0 + +# Install a specific branch +pip install git+https://github.com/ch-dev401/mongodb-query-builder.git@develop +``` + +## Basic Concepts + +MongoDB Query Builder provides three main components: + +1. **QueryFilter** - Build MongoDB query filters +2. **AggregateBuilder** - Create aggregation pipelines +3. **AtlasSearchBuilder** - Construct Atlas Search queries + +All builders follow a fluent interface pattern, allowing you to chain methods for readable query construction. + +## Your First Query + +Let's start with a simple example: + +```python +from mongodb_query_builder import QueryFilter + +# Create a simple query +query = QueryFilter() + .field("age").greater_than(18) + .field("status").equals("active") + .build() + +print(query) +# Output: {"age": {"$gt": 18}, "status": "active"} +``` + +## Using with PyMongo + +Here's how to use MongoDB Query Builder with PyMongo: + +```python +from pymongo import MongoClient +from mongodb_query_builder import QueryFilter + +# Connect to MongoDB +client = MongoClient("mongodb://localhost:27017/") +db = client["mydatabase"] +collection = db["users"] + +# Build and execute a query +query = QueryFilter() + .field("age").between(25, 35) + .field("city").equals("New York") + .build() + +# Find documents +results = collection.find(query) +for doc in results: + print(doc) +``` + +## Building Aggregation Pipelines + +Aggregation pipelines allow you to process and transform your data: + +```python +from mongodb_query_builder import AggregateBuilder, QueryFilter + +# Create an aggregation pipeline +pipeline = AggregateBuilder() + .match( + QueryFilter() + .field("status").equals("completed") + .field("amount").greater_than(100) + ) + .group( + by="$category", + total_amount={"$sum": "$amount"}, + count={"$sum": 1} + ) + .sort("total_amount", ascending=False) + .limit(5) + .build() + +# Execute with PyMongo +results = collection.aggregate(pipeline) +for doc in results: + print(f"{doc['_id']}: ${doc['total_amount']} ({doc['count']} items)") +``` + +## Working with Complex Queries + +### Logical Operators + +Combine multiple conditions using logical operators: + +```python +from mongodb_query_builder import QueryFilter + +# OR condition +query = QueryFilter().any_of([ + QueryFilter().field("role").equals("admin"), + QueryFilter().field("role").equals("moderator") +]).build() + +# AND condition (implicit) +query = QueryFilter() + .field("age").greater_than(18) + .field("status").equals("active") + .build() + +# Complex nested conditions +query = QueryFilter().all_of([ + QueryFilter().field("type").equals("premium"), + QueryFilter().any_of([ + QueryFilter().field("credits").greater_than(100), + QueryFilter().field("subscription").equals("unlimited") + ]) +]).build() +``` + +### Array Operations + +Work with array fields: + +```python +# Check if array contains a value +query = QueryFilter() + .field("tags").array_contains("python") + .build() + +# Check if array contains all values +query = QueryFilter() + .field("skills").array_contains_all(["python", "mongodb", "docker"]) + .build() + +# Check array size +query = QueryFilter() + .field("items").array_size(5) + .build() +``` + +### String Operations + +Perform string matching: + +```python +# Starts with +query = QueryFilter() + .field("name").starts_with("John") + .build() + +# Contains (case-insensitive) +query = QueryFilter() + .field("description").contains("python") + .build() + +# Regular expression +query = QueryFilter() + .field("email").regex(r".*@example\.com$") + .build() +``` + +## Atlas Search Example + +If you're using MongoDB Atlas, you can build search queries: + +```python +from mongodb_query_builder import AtlasSearchBuilder + +# Simple text search +search = AtlasSearchBuilder() + .text("python developer", path=["title", "description"]) + .build_stage() + +# Search with fuzzy matching +search = AtlasSearchBuilder() + .text("pythn", path="skills", fuzzy={"maxEdits": 2}) + .build_stage() + +# Use in aggregation pipeline +pipeline = [search, {"$limit": 10}] +results = collection.aggregate(pipeline) +``` + +## Best Practices + +### 1. Use Type Hints + +Take advantage of type hints for better IDE support: + +```python +from mongodb_query_builder import QueryFilter +from typing import Dict, Any + +def get_active_users(min_age: int) -> Dict[str, Any]: + return QueryFilter() + .field("age").greater_than_or_equal(min_age) + .field("status").equals("active") + .build() +``` + +### 2. Reuse Query Components + +Build reusable query components: + +```python +# Create reusable filters +def active_users_filter() -> QueryFilter: + return QueryFilter().field("status").equals("active") + +def premium_users_filter() -> QueryFilter: + return QueryFilter().field("subscription").in_(["premium", "enterprise"]) + +# Combine filters +query = QueryFilter().all_of([ + active_users_filter(), + premium_users_filter() +]).build() +``` + +### 3. Handle Errors Gracefully + +MongoDB Query Builder provides specific exceptions: + +```python +from mongodb_query_builder import QueryFilter, QueryFilterError + +try: + query = QueryFilter() + .field("age").greater_than("not a number") # This will raise an error + .build() +except QueryFilterError as e: + print(f"Query construction error: {e}") +``` + +## Next Steps + +Now that you understand the basics: + +1. Explore the [API Reference](api/query-filter.md) for detailed method documentation +2. Check out the [Tutorials](tutorials/01-basic-queries.md) for step-by-step guides +3. Browse the [Cookbook](cookbook/user-authentication.md) for real-world examples +4. Read the [Performance Guide](performance-guide.md) for optimization tips + +## Quick Reference + +Here's a quick reference of common operations: + +| Operation | Method | Example | +|-----------|---------|---------| +| Equals | `equals()` | `.field("status").equals("active")` | +| Not equals | `not_equals()` | `.field("status").not_equals("deleted")` | +| Greater than | `greater_than()` | `.field("age").greater_than(18)` | +| Less than | `less_than()` | `.field("price").less_than(100)` | +| Between | `between()` | `.field("age").between(18, 65)` | +| In array | `in_()` | `.field("role").in_(["admin", "user"])` | +| Contains | `array_contains()` | `.field("tags").array_contains("python")` | +| Exists | `exists()` | `.field("email").exists()` | +| Regex | `regex()` | `.field("name").regex("^John")` | + +## Getting Help + +If you need help: + +- Check the [Troubleshooting Guide](troubleshooting.md) +- Search existing [GitHub Issues](https://github.com/ch-dev401/mongodb-query-builder/issues) +- Ask questions in the [Discussions](https://github.com/ch-dev401/mongodb-query-builder/discussions) +- Report bugs via [GitHub Issues](https://github.com/ch-dev401/mongodb-query-builder/issues/new) + +Happy querying! 🚀 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..7677b7f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,122 @@ +# MongoDB Query Builder + +A Python library for building MongoDB queries with a clean, intuitive API. + +[![PyPI version](https://badge.fury.io/py/mongodb-query-builder.svg)](https://badge.fury.io/py/mongodb-query-builder) +[![Python versions](https://img.shields.io/pypi/pyversions/mongodb-query-builder.svg)](https://pypi.org/project/mongodb-query-builder/) +[![License](https://img.shields.io/pypi/l/mongodb-query-builder.svg)](https://github.com/yourusername/mongodb-builder/blob/main/LICENSE) +[![Documentation Status](https://readthedocs.org/projects/mongodb-query-builder/badge/?version=latest)](https://mongodb-query-builder.readthedocs.io/en/latest/?badge=latest) + +## Overview + +MongoDB Query Builder provides a type-safe, intuitive way to build complex MongoDB queries in Python. It supports: + +- ✅ **Type-safe query construction** - Full typing support with IDE autocomplete +- ✅ **Chainable API** - Build complex queries with method chaining +- ✅ **Aggregation pipelines** - Simplified aggregation pipeline construction +- ✅ **Atlas Search integration** - Native support for MongoDB Atlas Search +- ✅ **Comprehensive operators** - Support for all MongoDB query operators +- ✅ **Python 3.8+** - Modern Python support + +## Quick Example + +```python +from mongodb_query_builder import QueryFilter, AggregateBuilder + +# Simple query +query = QueryFilter().where("age").gte(18).where("status").eq("active") + +# Complex aggregation +pipeline = ( + AggregateBuilder() + .match({"category": "electronics"}) + .group({ + "_id": "$brand", + "total_sales": {"$sum": "$price"}, + "avg_rating": {"$avg": "$rating"} + }) + .sort("-total_sales") + .limit(10) + .build() +) +``` + +## Features + +### 🔍 Intuitive Query Building + +Build queries using a natural, chainable syntax: + +```python +query = ( + QueryFilter() + .where("price").between(100, 500) + .where("category").in_(["electronics", "computers"]) + .where("rating").gte(4.0) +) +``` + +### 🚀 Powerful Aggregation Pipelines + +Create complex aggregation pipelines with ease: + +```python +pipeline = ( + AggregateBuilder() + .match({"status": "shipped"}) + .lookup("customers", "customer_id", "_id", "customer_details") + .unwind("$customer_details") + .group({ + "_id": "$customer_details.country", + "total_orders": {"$sum": 1}, + "revenue": {"$sum": "$total_amount"} + }) + .build() +) +``` + +### 🔎 Atlas Search Integration + +Native support for MongoDB Atlas Search: + +```python +from mongodb_query_builder import AtlasSearchBuilder + +search = ( + AtlasSearchBuilder() + .text("laptop", path="title") + .range("price", gte=500, lte=2000) + .facet("category", "brand") + .highlight("description") + .build() +) +``` + +## Installation + +Install using pip: + +```bash +pip install mongodb-query-builder +``` + +Or with motor (async support): + +```bash +pip install mongodb-query-builder[motor] +``` + +## Documentation + +- [Getting Started](getting-started.md) - Installation and basic usage +- [User Guide](tutorials/01-basic-queries.md) - Comprehensive tutorials +- [API Reference](api/query-filter.md) - Complete API documentation +- [Cookbook](cookbook/index.md) - Real-world examples and patterns + +## Contributing + +We welcome contributions! Please see our [Contributing Guide](https://github.com/yourusername/mongodb-builder/blob/main/CONTRIBUTING.md) for details. + +## License + +This project is licensed under the MIT License - see the [LICENSE](https://github.com/yourusername/mongodb-builder/blob/main/LICENSE) file for details. diff --git a/docs/migration-guide.md b/docs/migration-guide.md new file mode 100644 index 0000000..29dc4d5 --- /dev/null +++ b/docs/migration-guide.md @@ -0,0 +1,690 @@ +# Migration Guide: From Raw MongoDB Queries to Query Builder + +This guide helps you convert existing MongoDB queries to use MongoDB Query Builder's fluent API. Each section shows common MongoDB query patterns and their Query Builder equivalents. + +## Why Migrate? + +- **Type Safety**: Catch errors at development time +- **Readability**: Self-documenting code +- **Maintainability**: Easier to modify and extend +- **IDE Support**: Auto-completion and inline documentation +- **Fewer Bugs**: Validated query construction + +## Basic Query Conversions + +### Simple Equality + +**MongoDB Query:** +```javascript +{ "status": "active" } +``` + +**Query Builder:** +```python +from mongodb_query_builder import QueryFilter + +query = QueryFilter() + .field("status").equals("active") + .build() +``` + +### Multiple Fields + +**MongoDB Query:** +```javascript +{ + "status": "active", + "age": 25, + "city": "New York" +} +``` + +**Query Builder:** +```python +query = QueryFilter() + .field("status").equals("active") + .field("age").equals(25) + .field("city").equals("New York") + .build() +``` + +## Comparison Operators + +### Greater Than + +**MongoDB Query:** +```javascript +{ "age": { "$gt": 18 } } +``` + +**Query Builder:** +```python +query = QueryFilter() + .field("age").greater_than(18) + .build() +``` + +### Range Query + +**MongoDB Query:** +```javascript +{ "price": { "$gte": 100, "$lte": 500 } } +``` + +**Query Builder:** +```python +query = QueryFilter() + .field("price").between(100, 500) + .build() +``` + +### Multiple Comparisons + +**MongoDB Query:** +```javascript +{ + "age": { "$gte": 21 }, + "income": { "$gt": 50000 }, + "credit_score": { "$gte": 700 } +} +``` + +**Query Builder:** +```python +query = QueryFilter() + .field("age").greater_than_or_equal(21) + .field("income").greater_than(50000) + .field("credit_score").greater_than_or_equal(700) + .build() +``` + +## Logical Operators + +### OR Condition + +**MongoDB Query:** +```javascript +{ + "$or": [ + { "status": "active" }, + { "status": "pending" } + ] +} +``` + +**Query Builder:** +```python +query = QueryFilter().any_of([ + QueryFilter().field("status").equals("active"), + QueryFilter().field("status").equals("pending") +]).build() + +# Alternative using in_ +query = QueryFilter() + .field("status").in_(["active", "pending"]) + .build() +``` + +### Complex AND/OR + +**MongoDB Query:** +```javascript +{ + "$and": [ + { "type": "premium" }, + { + "$or": [ + { "credits": { "$gt": 100 } }, + { "subscription": "unlimited" } + ] + } + ] +} +``` + +**Query Builder:** +```python +query = QueryFilter().all_of([ + QueryFilter().field("type").equals("premium"), + QueryFilter().any_of([ + QueryFilter().field("credits").greater_than(100), + QueryFilter().field("subscription").equals("unlimited") + ]) +]).build() +``` + +## Array Operations + +### Array Contains + +**MongoDB Query:** +```javascript +{ "tags": "python" } +``` + +**Query Builder:** +```python +query = QueryFilter() + .field("tags").array_contains("python") + .build() +``` + +### Array Contains All + +**MongoDB Query:** +```javascript +{ "skills": { "$all": ["python", "mongodb", "docker"] } } +``` + +**Query Builder:** +```python +query = QueryFilter() + .field("skills").array_contains_all(["python", "mongodb", "docker"]) + .build() +``` + +### Array Size + +**MongoDB Query:** +```javascript +{ "items": { "$size": 3 } } +``` + +**Query Builder:** +```python +query = QueryFilter() + .field("items").array_size(3) + .build() +``` + +## String Operations + +### Regex Patterns + +**MongoDB Query:** +```javascript +{ "email": { "$regex": "^[a-zA-Z0-9.]+@example\\.com$" } } +``` + +**Query Builder:** +```python +query = QueryFilter() + .field("email").regex(r"^[a-zA-Z0-9.]+@example\.com$") + .build() +``` + +### Case-Insensitive Contains + +**MongoDB Query:** +```javascript +{ "description": { "$regex": "mongodb", "$options": "i" } } +``` + +**Query Builder:** +```python +query = QueryFilter() + .field("description").contains("mongodb", case_sensitive=False) + .build() +``` + +### Starts With + +**MongoDB Query:** +```javascript +{ "username": { "$regex": "^admin" } } +``` + +**Query Builder:** +```python +query = QueryFilter() + .field("username").starts_with("admin") + .build() +``` + +## Element Operators + +### Field Exists + +**MongoDB Query:** +```javascript +{ "email": { "$exists": true } } +``` + +**Query Builder:** +```python +query = QueryFilter() + .field("email").exists() + .build() +``` + +### Type Check + +**MongoDB Query:** +```javascript +{ "age": { "$type": "number" } } +``` + +**Query Builder:** +```python +query = QueryFilter() + .field("age").type_check("number") + .build() +``` + +## Aggregation Pipeline Conversions + +### Basic Pipeline + +**MongoDB Pipeline:** +```javascript +[ + { "$match": { "status": "active" } }, + { "$group": { + "_id": "$category", + "count": { "$sum": 1 } + }}, + { "$sort": { "count": -1 } }, + { "$limit": 5 } +] +``` + +**Query Builder:** +```python +from mongodb_query_builder import AggregateBuilder, QueryFilter + +pipeline = AggregateBuilder() + .match(QueryFilter().field("status").equals("active")) + .group(by="$category", count={"$sum": 1}) + .sort("count", ascending=False) + .limit(5) + .build() +``` + +### Complex Aggregation + +**MongoDB Pipeline:** +```javascript +[ + { "$match": { + "created": { "$gte": ISODate("2024-01-01") } + }}, + { "$lookup": { + "from": "users", + "localField": "userId", + "foreignField": "_id", + "as": "user" + }}, + { "$unwind": "$user" }, + { "$group": { + "_id": { + "month": { "$month": "$created" }, + "userId": "$userId" + }, + "totalAmount": { "$sum": "$amount" }, + "orderCount": { "$sum": 1 } + }}, + { "$project": { + "month": "$_id.month", + "userId": "$_id.userId", + "totalAmount": 1, + "orderCount": 1, + "_id": 0 + }} +] +``` + +**Query Builder:** +```python +from datetime import datetime + +pipeline = AggregateBuilder() + .match( + QueryFilter() + .field("created").greater_than_or_equal(datetime(2024, 1, 1)) + ) + .lookup( + from_collection="users", + local_field="userId", + foreign_field="_id", + as_field="user" + ) + .unwind("$user") + .group( + by={ + "month": {"$month": "$created"}, + "userId": "$userId" + }, + totalAmount={"$sum": "$amount"}, + orderCount={"$sum": 1} + ) + .project( + month="$_id.month", + userId="$_id.userId", + totalAmount=1, + orderCount=1, + _id=0 + ) + .build() +``` + +## Atlas Search Conversions + +### Basic Text Search + +**Atlas Search Query:** +```javascript +{ + "$search": { + "text": { + "query": "mongodb python", + "path": ["title", "description"] + } + } +} +``` + +**Query Builder:** +```python +from mongodb_query_builder import AtlasSearchBuilder + +search = AtlasSearchBuilder() + .text("mongodb python", path=["title", "description"]) + .build_stage() +``` + +### Compound Search + +**Atlas Search Query:** +```javascript +{ + "$search": { + "compound": { + "must": [{ + "text": { + "query": "developer", + "path": "title" + } + }], + "should": [{ + "text": { + "query": "senior", + "path": "level", + "score": { "boost": { "value": 2.0 } } + } + }], + "filter": [{ + "range": { + "path": "experience", + "gte": 3 + } + }] + } + } +} +``` + +**Query Builder:** +```python +from mongodb_query_builder import AtlasSearchBuilder, CompoundBuilder + +compound = CompoundBuilder() +compound.must().text("developer", path="title") +compound.should().text("senior", path="level", score=2.0) +compound.filter().range("experience", gte=3) + +search = AtlasSearchBuilder() + .compound(compound) + .build_stage() +``` + +## Real-World Migration Examples + +### Example 1: User Query Migration + +**Original MongoDB Query:** +```python +# Raw query construction +query = { + "$and": [ + {"age": {"$gte": 18, "$lte": 65}}, + {"status": "active"}, + { + "$or": [ + {"role": "premium"}, + {"credits": {"$gt": 100}} + ] + }, + {"skills": {"$all": ["python", "mongodb"]}}, + {"email": {"$exists": True}} + ] +} +``` + +**Migrated to Query Builder:** +```python +query = QueryFilter() + .field("age").between(18, 65) + .field("status").equals("active") + .any_of([ + QueryFilter().field("role").equals("premium"), + QueryFilter().field("credits").greater_than(100) + ]) + .field("skills").array_contains_all(["python", "mongodb"]) + .field("email").exists() + .build() +``` + +### Example 2: Analytics Pipeline Migration + +**Original MongoDB Pipeline:** +```python +pipeline = [ + { + "$match": { + "timestamp": { + "$gte": start_date, + "$lt": end_date + }, + "event_type": {"$in": ["purchase", "upgrade"]} + } + }, + { + "$group": { + "_id": { + "date": {"$dateToString": {"format": "%Y-%m-%d", "date": "$timestamp"}}, + "event_type": "$event_type" + }, + "count": {"$sum": 1}, + "revenue": {"$sum": "$amount"} + } + }, + { + "$sort": {"_id.date": 1} + }, + { + "$facet": { + "daily_summary": [ + {"$group": { + "_id": "$_id.date", + "total_events": {"$sum": "$count"}, + "total_revenue": {"$sum": "$revenue"} + }} + ], + "event_breakdown": [ + {"$group": { + "_id": "$_id.event_type", + "total_count": {"$sum": "$count"}, + "total_revenue": {"$sum": "$revenue"} + }} + ] + } + } +] +``` + +**Migrated to Query Builder:** +```python +pipeline = AggregateBuilder() + .match( + QueryFilter() + .field("timestamp").between(start_date, end_date) + .field("event_type").in_(["purchase", "upgrade"]) + ) + .group( + by={ + "date": {"$dateToString": {"format": "%Y-%m-%d", "date": "$timestamp"}}, + "event_type": "$event_type" + }, + count={"$sum": 1}, + revenue={"$sum": "$amount"} + ) + .sort("_id.date", ascending=True) + .facet( + daily_summary=[ + {"$group": { + "_id": "$_id.date", + "total_events": {"$sum": "$count"}, + "total_revenue": {"$sum": "$revenue"} + }} + ], + event_breakdown=[ + {"$group": { + "_id": "$_id.event_type", + "total_count": {"$sum": "$count"}, + "total_revenue": {"$sum": "$revenue"} + }} + ] + ) + .build() +``` + +## Migration Best Practices + +### 1. Start with Simple Queries + +Begin by migrating your simplest queries first to get familiar with the API: + +```python +# Start with basic queries +old_query = {"status": "active", "verified": True} +new_query = QueryFilter() + .field("status").equals("active") + .field("verified").equals(True) + .build() +``` + +### 2. Create Helper Functions + +Build reusable functions for common query patterns: + +```python +def date_range_filter(field, start, end): + """Create a date range filter.""" + return QueryFilter().field(field).between(start, end) + +def active_items_filter(): + """Filter for active items.""" + return QueryFilter() + .field("status").equals("active") + .field("deleted").equals(False) +``` + +### 3. Test Incrementally + +Compare outputs during migration: + +```python +def test_query_migration(): + # Original query + old_query = {"age": {"$gte": 18}, "status": "active"} + + # New query + new_query = QueryFilter() + .field("age").greater_than_or_equal(18) + .field("status").equals("active") + .build() + + # Verify they're equivalent + assert old_query == new_query +``` + +### 4. Document Complex Migrations + +For complex queries, document the conversion: + +```python +# Original: Find users with recent activity and high engagement +# Query combines multiple conditions with complex logic +# +# Migration notes: +# - Converted $and to implicit AND (chained fields) +# - Simplified $or using any_of() +# - Used between() for date ranges + +query = QueryFilter() + .field("last_login").between(week_ago, today) + .field("engagement_score").greater_than(75) + .any_of([ + QueryFilter().field("posts_count").greater_than(10), + QueryFilter().field("comments_count").greater_than(50) + ]) + .build() +``` + +## Common Pitfalls and Solutions + +### Pitfall 1: Forgetting build() + +```python +# Wrong - returns QueryFilter object, not dict +query = QueryFilter().field("status").equals("active") + +# Correct - returns MongoDB query dict +query = QueryFilter().field("status").equals("active").build() +``` + +### Pitfall 2: Incorrect Logical Grouping + +```python +# MongoDB: { "$or": [ ... ] } at root level +# Wrong: +query = QueryFilter() + .field("a").equals(1) + .any_of([...]) # This creates an AND condition + +# Correct: +query = QueryFilter().any_of([ + QueryFilter().field("a").equals(1), + QueryFilter().field("b").equals(2) +]).build() +``` + +### Pitfall 3: Array Operations + +```python +# MongoDB: { "tags": "python" } matches array containing "python" +# This is already handled by array_contains: +query = QueryFilter() + .field("tags").array_contains("python") + .build() +``` + +## Migration Checklist + +- [ ] Identify all MongoDB queries in your codebase +- [ ] Start with read queries (find operations) +- [ ] Migrate simple queries first +- [ ] Create helper functions for repeated patterns +- [ ] Test each migration thoroughly +- [ ] Update any query-building utilities +- [ ] Migrate aggregation pipelines +- [ ] Migrate Atlas Search queries (if applicable) +- [ ] Update documentation and examples +- [ ] Train team on new syntax + +## Need Help? + +- Check the [API Reference](api/query-filter.md) for detailed method documentation +- Browse [Tutorials](tutorials/01-basic-queries.md) for step-by-step examples +- See the [Troubleshooting Guide](troubleshooting.md) for common issues +- Ask questions in [GitHub Discussions](https://github.com/ch-dev401/mongodb-query-builder/discussions) diff --git a/docs/performance-guide.md b/docs/performance-guide.md new file mode 100644 index 0000000..e06553c --- /dev/null +++ b/docs/performance-guide.md @@ -0,0 +1,485 @@ +# Performance Guide + +This guide provides best practices and optimization techniques for using MongoDB Query Builder efficiently. + +## Query Performance Fundamentals + +### 1. Use Indexes Effectively + +MongoDB queries perform best when they can use indexes. Design your queries to take advantage of existing indexes. + +#### Single Field Index + +```python +# Efficient: Uses index on status field +query = QueryFilter() + .field("status").equals("active") + .build() + +# Create index in MongoDB: +# db.collection.createIndex({"status": 1}) +``` + +#### Compound Index + +```python +# Efficient: Uses compound index +query = QueryFilter() + .field("status").equals("active") + .field("created_date").greater_than(datetime(2024, 1, 1)) + .field("category").equals("electronics") + .build() + +# Create compound index (order matters!): +# db.collection.createIndex({"status": 1, "created_date": -1, "category": 1}) +``` + +### 2. Filter Early in Pipelines + +Place `$match` stages as early as possible in aggregation pipelines to reduce the number of documents processed. + +```python +# Good: Filter early +pipeline = AggregateBuilder() + .match(QueryFilter().field("status").equals("active")) # Filter first + .lookup( + from_collection="orders", + local_field="_id", + foreign_field="userId", + as_field="orders" + ) + .unwind("$orders") + .group(by="$category", total={"$sum": "$orders.amount"}) + .build() + +# Bad: Filter late +pipeline = AggregateBuilder() + .lookup( + from_collection="orders", + local_field="_id", + foreign_field="userId", + as_field="orders" + ) + .unwind("$orders") + .match(QueryFilter().field("status").equals("active")) # Filter late + .group(by="$category", total={"$sum": "$orders.amount"}) + .build() +``` + +### 3. Limit Result Sets + +Always limit results when you don't need all documents. + +```python +# Good: Limit results +query = QueryFilter() + .field("category").equals("books") + .build() + +results = collection.find(query).limit(100) + +# Or in aggregation +pipeline = AggregateBuilder() + .match(QueryFilter().field("category").equals("books")) + .sort("rating", ascending=False) + .limit(100) + .build() +``` + +## Query Optimization Patterns + +### 1. Selective Field Projection + +Only retrieve fields you need to reduce network transfer and memory usage. + +```python +# Good: Select only needed fields +pipeline = AggregateBuilder() + .match(QueryFilter().field("status").equals("active")) + .project( + name=1, + email=1, + lastLogin=1, + _id=0 # Exclude _id if not needed + ) + .build() + +# With PyMongo +results = collection.find( + query, + {"name": 1, "email": 1, "lastLogin": 1, "_id": 0} +) +``` + +### 2. Efficient Range Queries + +Use appropriate operators for range queries to maximize index usage. + +```python +# Good: Single range with index +query = QueryFilter() + .field("price").between(100, 500) + .build() + +# Better: If you have a compound index on (category, price) +query = QueryFilter() + .field("category").equals("electronics") + .field("price").between(100, 500) + .build() +``` + +### 3. Optimize Array Operations + +Array operations can be expensive. Use them judiciously. + +```python +# Efficient: Single array check +query = QueryFilter() + .field("tags").array_contains("mongodb") + .build() + +# Less efficient: Multiple array conditions +query = QueryFilter() + .field("tags").array_contains_all(["mongodb", "python", "nosql"]) + .field("tags").array_size(5) + .build() + +# Consider restructuring data or using aggregation for complex array queries +``` + +### 4. Smart Logical Operations + +Structure logical operations for best performance. + +```python +# Good: Most selective condition first +query = QueryFilter() + .field("accountType").equals("premium") # Very selective + .field("status").equals("active") # Less selective + .build() + +# For OR conditions, put most likely matches first +query = QueryFilter().any_of([ + QueryFilter().field("priority").equals("high"), # Most common + QueryFilter().field("escalated").equals(True), # Less common + QueryFilter().field("vip_customer").equals(True) # Rare +]).build() +``` + +## Aggregation Pipeline Optimization + +### 1. Pipeline Stage Ordering + +MongoDB can optimize certain stage sequences. Follow these patterns: + +```python +# Optimal order for common operations +pipeline = AggregateBuilder() + # 1. Filter documents + .match(QueryFilter().field("year").equals(2024)) + + # 2. Sort (can use index if immediately after match) + .sort("date", ascending=True) + + # 3. Limit (combines with sort for top-k optimization) + .limit(100) + + # 4. Project to reduce document size + .project(date=1, amount=1, category=1) + + # 5. Additional processing + .group(by="$category", total={"$sum": "$amount"}) + .build() +``` + +### 2. Minimize Document Modifications + +Avoid unnecessary document reshaping in pipelines. + +```python +# Bad: Multiple reshaping stages +pipeline = AggregateBuilder() + .add_fields(temp1={"$multiply": ["$price", "$quantity"]}) + .add_fields(temp2={"$multiply": ["$temp1", 0.9]}) + .add_fields(final={"$round": ["$temp2", 2]}) + .build() + +# Good: Single computation stage +pipeline = AggregateBuilder() + .add_fields( + final={ + "$round": [ + {"$multiply": [ + {"$multiply": ["$price", "$quantity"]}, + 0.9 + ]}, + 2 + ] + } + ) + .build() +``` + +### 3. Use allowDiskUse for Large Datasets + +For aggregations processing large amounts of data: + +```python +# Enable disk use for large aggregations +pipeline = AggregateBuilder() + .match(QueryFilter().field("year").equals(2024)) + .group(by="$customerId", totalSpent={"$sum": "$amount"}) + .sort("totalSpent", ascending=False) + .build() + +# Execute with allowDiskUse +results = collection.aggregate(pipeline, allowDiskUse=True) +``` + +### 4. Optimize $lookup Operations + +Lookups can be expensive. Optimize them carefully. + +```python +# Good: Filter before lookup +pipeline = AggregateBuilder() + .match(QueryFilter().field("status").equals("active")) + .lookup( + from_collection="orders", + local_field="_id", + foreign_field="userId", + as_field="orders" + ) + .build() + +# Better: Use pipeline lookup for filtering joined documents +lookup_pipeline = [ + {"$match": {"$expr": {"$eq": ["$userId", "$$user_id"]}}}, + {"$match": {"status": "completed"}}, + {"$limit": 10} +] + +pipeline = AggregateBuilder() + .add_stage({ + "$lookup": { + "from": "orders", + "let": {"user_id": "$_id"}, + "pipeline": lookup_pipeline, + "as": "recent_orders" + } + }) + .build() +``` + +## Atlas Search Performance + +### 1. Index Configuration + +Configure Atlas Search indexes for optimal performance: + +```python +# Use specific paths instead of dynamic mapping +# Index configuration (in Atlas): +{ + "mappings": { + "fields": { + "title": { + "type": "string", + "analyzer": "lucene.standard" + }, + "description": { + "type": "string", + "analyzer": "lucene.standard" + }, + "category": { + "type": "string", + "analyzer": "lucene.keyword" + } + } + } +} +``` + +### 2. Efficient Search Queries + +```python +# Good: Use compound queries with filters +compound = CompoundBuilder() +compound.must().text("laptop", path="title") +compound.filter().equals("category", "electronics") +compound.filter().range("price", lte=1000) + +search = AtlasSearchBuilder() + .compound(compound) + .build_stage() + +# Bad: Text search without filters +search = AtlasSearchBuilder() + .text("laptop", path=["title", "description", "specs", "reviews"]) + .build_stage() +``` + +### 3. Limit Search Fields + +Search only necessary fields: + +```python +# Good: Specific fields +search = AtlasSearchBuilder() + .text("mongodb", path=["title", "tags"]) + .build_stage() + +# Bad: Searching all fields +search = AtlasSearchBuilder() + .text("mongodb", path="*") # Searches all fields + .build_stage() +``` + +## Query Patterns to Avoid + +### 1. Negation Operators + +Negation operators often can't use indexes efficiently: + +```python +# Inefficient: Negation +query = QueryFilter() + .field("status").not_equals("inactive") + .build() + +# Better: Positive match +query = QueryFilter() + .field("status").in_(["active", "pending", "processing"]) + .build() +``` + +### 2. Complex Regular Expressions + +Avoid unanchored regex patterns: + +```python +# Bad: Unanchored regex (full collection scan) +query = QueryFilter() + .field("email").regex(r".*@example\.com") + .build() + +# Good: Anchored regex (can use index) +query = QueryFilter() + .field("email").regex(r"^user.*@example\.com$") + .build() + +# Better: Use specific operators +query = QueryFilter() + .field("email").ends_with("@example.com") + .build() +``` + +### 3. Large $in Arrays + +Avoid very large arrays in `$in` operators: + +```python +# Bad: Huge array +large_list = [str(i) for i in range(10000)] +query = QueryFilter() + .field("userId").in_(large_list) + .build() + +# Better: Use ranges or multiple queries +query = QueryFilter().any_of([ + QueryFilter().field("userIdPrefix").equals("groupA"), + QueryFilter().field("userIdPrefix").equals("groupB") +]).build() +``` + +## Monitoring and Analysis + +### 1. Use explain() to Analyze Queries + +```python +# Analyze query performance +query = QueryFilter() + .field("status").equals("active") + .field("category").equals("electronics") + .build() + +# Get execution stats +explanation = collection.find(query).explain("executionStats") + +# Check if index was used +print(explanation["executionStats"]["totalDocsExamined"]) +print(explanation["executionStats"]["totalKeysExamined"]) +print(explanation["executionStats"]["executionTimeMillis"]) +``` + +### 2. Monitor Slow Queries + +Enable MongoDB profiling to identify slow queries: + +```python +# Enable profiling for slow queries (> 100ms) +db.setProfilingLevel(1, {"slowms": 100}) + +# Query the profile collection +slow_queries = db.system.profile.find({ + "millis": {"$gt": 100} +}).sort("millis", -1).limit(10) +``` + +### 3. Index Usage Statistics + +Monitor index usage: + +```python +# Get index statistics +index_stats = db.collection.aggregate([ + {"$indexStats": {}} +]) + +for stat in index_stats: + print(f"Index: {stat['name']}") + print(f"Usage: {stat['accesses']['ops']}") +``` + +## Best Practices Summary + +### Do's ✅ + +1. **Use indexes** - Design queries to leverage indexes +2. **Filter early** - Reduce document count as soon as possible +3. **Project selectively** - Only retrieve needed fields +4. **Limit results** - Use limit() for better performance +5. **Monitor performance** - Use explain() and profiling +6. **Cache results** - Cache frequently accessed, rarely changing data +7. **Batch operations** - Use bulk operations for multiple updates + +### Don'ts ❌ + +1. **Avoid collection scans** - Queries without index usage +2. **Don't over-fetch** - Retrieving more documents than needed +3. **Avoid complex regex** - Unanchored patterns are slow +4. **Limit array operations** - Complex array queries are expensive +5. **Avoid deep nesting** - Deeply nested queries are hard to optimize +6. **Don't ignore cardinality** - High-cardinality fields make better indexes + +## Performance Checklist + +Before deploying queries to production: + +- [ ] Indexes exist for all query patterns +- [ ] Queries tested with explain() +- [ ] Result sets are limited appropriately +- [ ] Only necessary fields are projected +- [ ] Aggregation pipelines filter early +- [ ] No unanchored regex patterns +- [ ] Array operations are minimized +- [ ] Compound indexes match query patterns +- [ ] Monitoring is in place for slow queries +- [ ] Load testing performed on realistic data + +## Additional Resources + +- [MongoDB Performance Best Practices](https://docs.mongodb.com/manual/administration/analyzing-mongodb-performance/) +- [Index Strategies](https://docs.mongodb.com/manual/applications/indexes/) +- [Aggregation Pipeline Optimization](https://docs.mongodb.com/manual/core/aggregation-pipeline-optimization/) +- [Atlas Search Performance](https://docs.atlas.mongodb.com/atlas-search/performance/) diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..e6bc883 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,30 @@ +# Documentation dependencies for MongoDB Query Builder + +# MkDocs core +mkdocs>=1.6.0 +mkdocs-material>=9.5.0 + +# MkDocs plugins +mkdocstrings>=0.25.0 +mkdocstrings-python>=1.10.0 +mkdocs-autorefs>=1.0.0 + +# MkDocs Material extensions +mkdocs-material-extensions>=1.3.0 + +# Python dependencies for autodoc +griffe>=0.45.0 + +# Markdown extensions +pymdown-extensions>=10.7.0 +markdown>=3.5.0 + +# For PDF generation +mkdocs-pdf-export-plugin>=0.5.10 + +# For versioning +mike>=2.0.0 + +# Additional tools +mkdocs-minify-plugin>=0.8.0 +mkdocs-redirects>=1.2.0 diff --git a/docs/tutorials/01-basic-queries.md b/docs/tutorials/01-basic-queries.md new file mode 100644 index 0000000..2a4c934 --- /dev/null +++ b/docs/tutorials/01-basic-queries.md @@ -0,0 +1,503 @@ +# Tutorial 1: Basic Queries with MongoDB Query Builder + +In this tutorial, you'll learn how to use MongoDB Query Builder to create basic queries. We'll start with simple examples and gradually build more complex queries. + +## Prerequisites + +- Python 3.8 or higher installed +- MongoDB Query Builder installed (`pip install mongodb-query-builder`) +- Basic understanding of MongoDB query concepts + +## Setting Up + +First, let's import the necessary components: + +```python +from mongodb_query_builder import QueryFilter +from pymongo import MongoClient + +# Connect to MongoDB (optional - for running queries) +client = MongoClient("mongodb://localhost:27017/") +db = client["tutorial_db"] +collection = db["users"] +``` + +## Lesson 1: Simple Equality Queries + +The most basic query checks if a field equals a specific value. + +### Example: Find Active Users + +```python +# Build the query +query = QueryFilter() + .field("status").equals("active") + .build() + +print(query) +# Output: {"status": "active"} + +# Use with PyMongo +active_users = collection.find(query) +``` + +### Example: Find Users by Age + +```python +query = QueryFilter() + .field("age").equals(25) + .build() + +# Output: {"age": 25} +``` + +## Lesson 2: Comparison Operators + +MongoDB Query Builder supports all standard comparison operators. + +### Greater Than / Less Than + +```python +# Find users older than 18 +query = QueryFilter() + .field("age").greater_than(18) + .build() +# Output: {"age": {"$gt": 18}} + +# Find products under $50 +query = QueryFilter() + .field("price").less_than(50) + .build() +# Output: {"price": {"$lt": 50}} +``` + +### Greater/Less Than or Equal + +```python +# Find users 18 or older +query = QueryFilter() + .field("age").greater_than_or_equal(18) + .build() +# Output: {"age": {"$gte": 18}} + +# Find items with 10 or fewer in stock +query = QueryFilter() + .field("stock").less_than_or_equal(10) + .build() +# Output: {"stock": {"$lte": 10}} +``` + +### Between (Range Queries) + +```python +# Find users between 25 and 35 years old +query = QueryFilter() + .field("age").between(25, 35) + .build() +# Output: {"age": {"$gte": 25, "$lte": 35}} + +# Find products in a price range +query = QueryFilter() + .field("price").between(100, 500) + .build() +# Output: {"price": {"$gte": 100, "$lte": 500}} +``` + +## Lesson 3: Working with Multiple Fields + +You can query multiple fields by chaining field operations. + +### Example: Multiple Conditions + +```python +# Find active users over 18 +query = QueryFilter() + .field("status").equals("active") + .field("age").greater_than(18) + .build() + +print(query) +# Output: {"status": "active", "age": {"$gt": 18}} +``` + +### Example: Complex User Query + +```python +# Find premium users in New York aged 25-40 +query = QueryFilter() + .field("account_type").equals("premium") + .field("city").equals("New York") + .field("age").between(25, 40) + .build() + +# Output: { +# "account_type": "premium", +# "city": "New York", +# "age": {"$gte": 25, "$lte": 40} +# } +``` + +## Lesson 4: Array Operations + +MongoDB Query Builder provides methods for querying array fields. + +### Checking Array Contents + +```python +# Find users with "python" in their skills +query = QueryFilter() + .field("skills").array_contains("python") + .build() +# Output: {"skills": "python"} + +# Find users with multiple specific skills +query = QueryFilter() + .field("skills").array_contains_all(["python", "mongodb", "javascript"]) + .build() +# Output: {"skills": {"$all": ["python", "mongodb", "javascript"]}} +``` + +### Array Size + +```python +# Find users with exactly 3 hobbies +query = QueryFilter() + .field("hobbies").array_size(3) + .build() +# Output: {"hobbies": {"$size": 3}} +``` + +## Lesson 5: String Operations + +Query text fields with string-specific operations. + +### Pattern Matching + +```python +# Find users whose name starts with "John" +query = QueryFilter() + .field("name").starts_with("John") + .build() +# Output: {"name": {"$regex": "^John"}} + +# Find emails ending with "@example.com" +query = QueryFilter() + .field("email").ends_with("@example.com") + .build() +# Output: {"email": {"$regex": "@example\\.com$"}} + +# Find descriptions containing "python" (case-insensitive) +query = QueryFilter() + .field("description").contains("python", case_sensitive=False) + .build() +# Output: {"description": {"$regex": "python", "$options": "i"}} +``` + +### Regular Expressions + +```python +# Find usernames matching a pattern +query = QueryFilter() + .field("username").regex(r"^user_\d{4}$") + .build() +# Output: {"username": {"$regex": "^user_\\d{4}$"}} +``` + +## Lesson 6: Logical Operators + +Combine multiple conditions using logical operators. + +### OR Conditions + +```python +# Find users who are either admins or moderators +query = QueryFilter().any_of([ + QueryFilter().field("role").equals("admin"), + QueryFilter().field("role").equals("moderator") +]).build() + +# Output: {"$or": [{"role": "admin"}, {"role": "moderator"}]} +``` + +### AND Conditions (Explicit) + +```python +# Find active premium users +query = QueryFilter().all_of([ + QueryFilter().field("status").equals("active"), + QueryFilter().field("account_type").equals("premium") +]).build() + +# Output: {"$and": [{"status": "active"}, {"account_type": "premium"}]} +``` + +### Complex Logical Combinations + +```python +# Find active users who are either premium or have high engagement +query = QueryFilter().all_of([ + QueryFilter().field("status").equals("active"), + QueryFilter().any_of([ + QueryFilter().field("account_type").equals("premium"), + QueryFilter().field("engagement_score").greater_than(80) + ]) +]).build() + +# Output: { +# "$and": [ +# {"status": "active"}, +# {"$or": [ +# {"account_type": "premium"}, +# {"engagement_score": {"$gt": 80}} +# ]} +# ] +# } +``` + +## Lesson 7: Checking Field Existence + +Query based on whether fields exist in documents. + +```python +# Find users with an email address +query = QueryFilter() + .field("email").exists() + .build() +# Output: {"email": {"$exists": true}} + +# Find users without a middle name +query = QueryFilter() + .field("middle_name").exists(False) + .build() +# Output: {"middle_name": {"$exists": false}} +``` + +## Lesson 8: Working with Nested Fields + +Query nested document fields using dot notation. + +```python +# Find users in New York +query = QueryFilter() + .field("address.city").equals("New York") + .build() +# Output: {"address.city": "New York"} + +# Complex nested query +query = QueryFilter() + .field("address.city").equals("San Francisco") + .field("address.zipcode").starts_with("94") + .field("profile.verified").equals(True) + .build() + +# Output: { +# "address.city": "San Francisco", +# "address.zipcode": {"$regex": "^94"}, +# "profile.verified": true +# } +``` + +## Practical Examples + +### Example 1: E-commerce Product Search + +```python +def find_discounted_electronics(min_discount=0.1, max_price=1000): + """Find electronic products on sale.""" + return QueryFilter() + .field("category").equals("electronics") + .field("discount").greater_than_or_equal(min_discount) + .field("price").less_than_or_equal(max_price) + .field("in_stock").equals(True) + .build() + +# Usage +query = find_discounted_electronics(min_discount=0.2, max_price=500) +products = collection.find(query) +``` + +### Example 2: User Search with Multiple Criteria + +```python +def find_qualified_users(min_age=21, required_skills=None, locations=None): + """Find users matching job requirements.""" + query = QueryFilter() + .field("age").greater_than_or_equal(min_age) + .field("profile_complete").equals(True) + + if required_skills: + query.field("skills").array_contains_all(required_skills) + + if locations: + query.field("location").in_(locations) + + return query.build() + +# Usage +query = find_qualified_users( + min_age=25, + required_skills=["python", "mongodb"], + locations=["New York", "San Francisco", "Austin"] +) +``` + +### Example 3: Content Filtering + +```python +def find_recent_posts(days=7, min_likes=10, tags=None): + """Find popular recent posts.""" + from datetime import datetime, timedelta + + cutoff_date = datetime.now() - timedelta(days=days) + + query = QueryFilter() + .field("created_at").greater_than_or_equal(cutoff_date) + .field("likes").greater_than_or_equal(min_likes) + .field("status").equals("published") + + if tags: + query.field("tags").array_contains(tags[0]) + + return query.build() + +# Usage +query = find_recent_posts(days=3, min_likes=50, tags=["mongodb"]) +``` + +## Best Practices + +### 1. Use Type-Appropriate Methods + +```python +# Good: Using numeric comparison for numbers +query = QueryFilter().field("age").greater_than(18) + +# Bad: Using string methods for numbers +# query = QueryFilter().field("age").starts_with("1") # Don't do this! +``` + +### 2. Build Reusable Query Functions + +```python +def active_users_filter(): + """Reusable filter for active users.""" + return QueryFilter().field("status").equals("active") + +def verified_users_filter(): + """Reusable filter for verified users.""" + return QueryFilter() + .field("email_verified").equals(True) + .field("phone_verified").equals(True) + +# Combine filters +query = QueryFilter().all_of([ + active_users_filter(), + verified_users_filter() +]).build() +``` + +### 3. Handle Optional Parameters + +```python +def build_user_query(status=None, min_age=None, city=None): + """Build a query with optional parameters.""" + query = QueryFilter() + + if status: + query.field("status").equals(status) + + if min_age is not None: + query.field("age").greater_than_or_equal(min_age) + + if city: + query.field("address.city").equals(city) + + return query.build() +``` + +## Exercises + +### Exercise 1: Product Catalog +Create a query to find all products that: +- Are in the "books" category +- Cost between $10 and $50 +- Have a rating of 4 or higher +- Are currently in stock + +
+Solution + +```python +query = QueryFilter() + .field("category").equals("books") + .field("price").between(10, 50) + .field("rating").greater_than_or_equal(4) + .field("in_stock").equals(True) + .build() +``` +
+ +### Exercise 2: User Matching +Create a query to find users who: +- Are between 25 and 40 years old +- Live in either "London", "Paris", or "Berlin" +- Have "python" in their skills +- Have verified their email + +
+Solution + +```python +query = QueryFilter() + .field("age").between(25, 40) + .field("city").in_(["London", "Paris", "Berlin"]) + .field("skills").array_contains("python") + .field("email_verified").equals(True) + .build() +``` +
+ +### Exercise 3: Complex Search +Create a query to find blog posts that: +- Were created in the last 30 days +- Have either more than 100 likes OR are featured +- Contain the tag "tutorial" +- Have a title starting with "How to" + +
+Solution + +```python +from datetime import datetime, timedelta + +thirty_days_ago = datetime.now() - timedelta(days=30) + +query = QueryFilter() + .field("created_at").greater_than_or_equal(thirty_days_ago) + .field("tags").array_contains("tutorial") + .field("title").starts_with("How to") + .any_of([ + QueryFilter().field("likes").greater_than(100), + QueryFilter().field("featured").equals(True) + ]) + .build() +``` +
+ +## Summary + +In this tutorial, you learned how to: +- Create simple equality queries +- Use comparison operators for numeric and date fields +- Query multiple fields in a single operation +- Work with arrays and string patterns +- Combine conditions using logical operators +- Query nested document fields +- Build reusable query functions + +## Next Steps + +- Continue to [Tutorial 2: Aggregation Pipelines](02-aggregation-pipelines.md) +- Explore the [QueryFilter API Reference](../api/query-filter.md) +- Try building queries for your own data models +- Learn about performance optimization in the [Performance Guide](../performance-guide.md) diff --git a/docs/tutorials/02-aggregation-pipelines.md b/docs/tutorials/02-aggregation-pipelines.md new file mode 100644 index 0000000..45a7b3a --- /dev/null +++ b/docs/tutorials/02-aggregation-pipelines.md @@ -0,0 +1,3 @@ +# Aggregation Pipelines + +This tutorial is coming soon! \ No newline at end of file diff --git a/docs/tutorials/03-atlas-search.md b/docs/tutorials/03-atlas-search.md new file mode 100644 index 0000000..7f3dcda --- /dev/null +++ b/docs/tutorials/03-atlas-search.md @@ -0,0 +1,3 @@ +# Atlas Search + +This tutorial is coming soon! \ No newline at end of file diff --git a/docs/tutorials/04-advanced-patterns.md b/docs/tutorials/04-advanced-patterns.md new file mode 100644 index 0000000..95c544a --- /dev/null +++ b/docs/tutorials/04-advanced-patterns.md @@ -0,0 +1,3 @@ +# Advanced Patterns + +This tutorial is coming soon! \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..492d87d --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,120 @@ +site_name: MongoDB Query Builder +site_description: A Python library for building MongoDB queries with a clean, intuitive API +site_author: Your Name +site_url: https://mongodb-query-builder.readthedocs.io/ + +repo_name: mongodb-query-builder +repo_url: https://github.com/ch-dev401/mongodb-query-builder +edit_uri: edit/main/docs/ + +theme: + name: material + palette: + - scheme: default + primary: green + accent: light-green + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: green + accent: light-green + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.tabs + - navigation.sections + - navigation.expand + - navigation.top + - navigation.instant + - navigation.tracking + - search.suggest + - search.highlight + - content.code.copy + - content.code.annotate + - content.tabs.link + icon: + repo: fontawesome/brands/github + +plugins: + - search: + separator: '[\s\-\_\.]+' + - mkdocstrings: + handlers: + python: + paths: [src] + options: + show_source: true + show_root_heading: true + show_category_heading: true + show_symbol_type_heading: true + members_order: source + docstring_style: google + merge_init_into_class: true + separate_signature: true + show_signature_annotations: true + signature_crossrefs: true + +nav: + - Home: index.md + - Getting Started: + - Installation: getting-started.md + - Quick Start: tutorials/01-basic-queries.md + - User Guide: + - Basic Queries: tutorials/01-basic-queries.md + - Aggregation Pipelines: tutorials/02-aggregation-pipelines.md + - Atlas Search: tutorials/03-atlas-search.md + - Advanced Patterns: tutorials/04-advanced-patterns.md + - API Reference: + - QueryFilter: api/query-filter.md + - AggregateBuilder: api/aggregate-builder.md + - AtlasSearchBuilder: api/atlas-search-builder.md + - Guides: + - Migration Guide: migration-guide.md + - Performance Guide: performance-guide.md + - Cookbook: + - Overview: cookbook/index.md + - Authentication Patterns: cookbook/authentication.md + - Product Catalog: cookbook/product-catalog.md + - Analytics Pipelines: cookbook/analytics.md + - Time Series Data: cookbook/time-series.md + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + - attr_list + - md_in_html + - toc: + permalink: true + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/ch-dev401/mongodb-query-builder + - icon: fontawesome/brands/python + link: https://pypi.org/project/mongodb-query-builder/ + version: + provider: mike + default: stable + +extra_css: + - assets/stylesheets/extra.css + +extra_javascript: + - https://unpkg.com/mermaid@10/dist/mermaid.min.js diff --git a/requiremets-dev.txt b/requiremets-dev.txt index e8aed18..35df580 100644 --- a/requiremets-dev.txt +++ b/requiremets-dev.txt @@ -18,11 +18,17 @@ pre-commit>=4.3.0 build>=1.3.0 twine>=6.2.0 -# Documentation +# Documentation - Sphinx (legacy) sphinx>=8.2.3 sphinx-rtd-theme>=3.0.2 sphinx-autodoc-typehints>=3.2.0 +# Documentation - MkDocs +mkdocs>=1.6.0 +mkdocs-material>=9.5.0 +mkdocstrings>=0.25.0 +mkdocstrings-python>=1.10.0 + # Optional runtime dependencies (for development) motor==3.7.1 -pymongo==4.15.1 \ No newline at end of file +pymongo==4.15.1