# filter-engine > Filter apartment posts by criteria - Author: OpenClaw User - Repository: ChenKrembo/aptfinder - Version: 20260131200051 - Stars: 0 - Forks: 0 - Last Updated: 2026-02-06 - Source: https://github.com/ChenKrembo/aptfinder - Web: https://mule.run/skillshub/@@ChenKrembo/aptfinder~filter-engine:20260131200051 --- --- description: Filter apartment posts by criteria invoke: claude --- # Filter Engine Skill ## When to Use - Modifying filter logic - Adding new filter criteria - Understanding why posts pass/fail - Debugging filter results - Adjusting scoring system ## Key Files - `src/filters/filter_engine.py` - FilterEngine class and logic - `src/crawler/base.py` - Post dataclass definition - `config/filters.json` - Filter configuration - `src/config.py` - FiltersConfig dataclass ## Core Classes ### Post Represents an apartment listing: ```python @dataclass class Post: id: str # Unique identifier source: str # "facebook", "mock", etc. url: str # Link to original post text: str # Full post text posted_at: datetime # When posted rooms: float | None # Number of rooms (e.g., 2.5) price: int | None # Monthly rent in NIS address: str | None # Extracted address ``` ### FilterResult Returned by filter checks: ```python @dataclass class FilterResult: passed: bool # Overall pass/fail post: Post # The checked post reasons: list[str] # Why it failed (if any) score: int # Quality score (higher = better match) ``` ### FilterEngine Main filtering class: ```python from src.filters import FilterEngine from src.config import load_config config = load_config() filter_engine = FilterEngine(config.filters) result = filter_engine.check(post) if result.passed: print(f"Match! Score: {result.score}") else: print(f"Filtered out: {result.reasons}") ``` ## Filter Criteria | Criteria | Config | Pass Condition | Fail Message | |----------|--------|----------------|--------------| | Rooms | `filters.rooms.min/max` | min <= rooms <= max | "Too few/many rooms" | | Price | `filters.price.min/max` | min <= price <= max | "Price too low/high" | | Keywords | `filters.keywords.must_exclude` | No excluded keywords | "Contains excluded keyword" | ## Scoring System The engine calculates a quality score for passed posts: | Condition | Points | |-----------|--------| | Rooms in range | +30 | | Price in range | +30 | | Each must_include keyword found | +10 | Higher scores = better matches. Use for sorting/prioritization. ## Configuration `config/filters.json`: ```json { "rooms": { "min": 2.5, "max": 3 }, "price": { "min": 4000, "max": 8000 }, "keywords": { "must_include": ["מרפסת", "חניה"], "must_exclude": ["סאבלט", "שותפים", "זמני"] } } ``` ## Filter Logic Walkthrough ```python def check(self, post: Post) -> FilterResult: reasons = [] score = 0 # 1. Check rooms (if available) if post.rooms is not None: if post.rooms < self.config.rooms.min: reasons.append(f"Too few rooms: {post.rooms}") elif post.rooms > self.config.rooms.max: reasons.append(f"Too many rooms: {post.rooms}") else: score += 30 # 2. Check price (if available) if post.price is not None: if post.price < self.config.price.min: reasons.append(f"Price too low: {post.price} (suspicious)") elif post.price > self.config.price.max: reasons.append(f"Price too high: {post.price}") else: score += 30 # 3. Check excluded keywords for kw in self.config.keywords.must_exclude: if kw in post.text: reasons.append(f"Contains excluded keyword: {kw}") # 4. Score for included keywords for kw in self.config.keywords.must_include: if kw in post.text: score += 10 passed = len(reasons) == 0 return FilterResult(passed=passed, post=post, reasons=reasons, score=score) ``` ## Example: Add New Filter ### 1. Add Configuration In `src/config.py`: ```python @dataclass class SquareMetersFilter: min: int = 40 max: int = 100 @dataclass class FiltersConfig: rooms: RoomsFilter = field(default_factory=RoomsFilter) price: PriceFilter = field(default_factory=PriceFilter) keywords: KeywordsFilter = field(default_factory=KeywordsFilter) sqm: SquareMetersFilter = field(default_factory=SquareMetersFilter) ``` ### 2. Update Post Dataclass In `src/crawler/base.py`: ```python @dataclass class Post: # ... existing fields ... sqm: int | None = None # Square meters ``` ### 3. Add Filter Logic In `src/filters/filter_engine.py`: ```python def check(self, post: Post) -> FilterResult: # ... existing checks ... # Check square meters if post.sqm is not None: if post.sqm < self.config.sqm.min: reasons.append(f"Too small: {post.sqm}m²") elif post.sqm > self.config.sqm.max: reasons.append(f"Too large: {post.sqm}m²") else: score += 20 ``` ### 4. Update Config File In `config/filters.json`: ```json { "sqm": { "min": 40, "max": 100 } } ``` ## Notification Integration The console notifier displays filter results: ```python from src.notifications import ConsoleNotifier notifier = ConsoleNotifier() result = filter_engine.check(post) if result.passed: notifier.notify(result) # Prints formatted match ``` ## Testing ```bash # Run filter tests python -m uv run pytest tests/test_filters.py -v # Test with mock data python -m uv run python main.py --once ``` ## Tips 1. **Missing data handling:** Filters skip criteria when data is `None` (e.g., no price = no price filter) 2. **Hebrew keywords:** Keywords are case-sensitive and match as substrings 3. **Score usage:** Sort matched posts by score for prioritization 4. **Suspicious prices:** Very low prices trigger warnings (might be scams)