Visitar URL original
feat: Added global search api and necessary unit tests by aniketpalu · Pull Request #5532 · feast-dev/feast · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Re-usable & modular dependency functions, removal of unnecessary fiel…
…ds, updated & added test cases, documentation update

Signed-off-by: Aniket Paluskar <apaluska@redhat.com>
  • Loading branch information
aniketpalu committed Jul 30, 2025
commit 3c71804cd8927b02fc8f9adeededcbeb9a19f3b4
108 changes: 108 additions & 0 deletions docs/reference/feature-servers/registry-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -1094,6 +1094,114 @@ Please refer the [page](./../../../docs/getting-started/concepts/permission.md)

**Note**: Recent visits are automatically logged when users access registry objects via the REST API. The logging behavior can be configured through the `feature_server.recent_visit_logging` section in `feature_store.yaml` (see configuration section below).


### Search API

#### Search Resources
- **Endpoint**: `GET /api/v1/search`
- **Description**: Search across all Feast resources including entities, feature views, features, feature services, data sources, and saved datasets. Supports cross-project search, fuzzy matching, relevance scoring, and advanced filtering.
- **Parameters**:
- `query` (required): Search query string. Searches in resource names, descriptions, and tags
- `projects` (optional): List of project names to search in. If not specified, searches all projects
- `allow_cache` (optional, default: `true`): Whether to allow cached data
- `tags` (optional): Filter results by tags in key=value format (e.g., `tags=environment:production&tags=team:ml`)
- `page` (optional, default: `1`): Page number for pagination
- `limit` (optional, default: `50`, max: `100`): Number of items per page
- `sort_by` (optional, default: `match_score`): Field to sort by (`match_score`, `name`, or `type`)
- `sort_order` (optional, default: `desc`): Sort order ("asc" or "desc")
- **Examples**:
```bash
# Basic search across all projects
curl -H "Authorization: Bearer <token>" \
"http://localhost:6572/api/v1/search?query=user"

# Search in specific projects
curl -H "Authorization: Bearer <token>" \
"http://localhost:6572/api/v1/search?query=driver&projects=ride_sharing&projects=analytics"

# Search with tag filtering
curl -H "Authorization: Bearer <token>" \
"http://localhost:6572/api/v1/search?query=features&tags=environment:production&tags=team:ml"

# Search with pagination and sorting
curl -H "Authorization: Bearer <token>" \
"http://localhost:6572/api/v1/search?query=conv_rate&page=1&limit=10&sort_by=name&sort_order=asc"

# Empty query to list all resources with filtering
curl -H "Authorization: Bearer <token>" \
"http://localhost:6572/api/v1/search?query=&projects=my_project&page=1&limit=20"
```
- **Response Example**:
```json
{
"query": "user",
"projects_searched": ["project1", "project2"],
"results": [
{
"type": "entity",
"name": "user_id",
"description": "Primary identifier for users",
"project": "project1",
"match_score": 100
},
{
"type": "featureView",
"name": "user_features",
"description": "User demographic and behavioral features",
"project": "project1",
"match_score": 100
},
{
"type": "feature",
"name": "user_age",
"description": "Age of the user in years",
"project": "project1",
"match_score": 80
},
{
"type": "dataSource",
"name": "user_analytics",
"description": "Analytics data for user behavior tracking",
"project": "project2",
"match_score": 80
}
],
"pagination": {
"page": 1,
"limit": 50,
"total_count": 4,
"total_pages": 1,
"has_next": false,
"has_previous": false
}
}
```
- **Project Handling**:
- **No projects specified**: Searches all available projects
- **Single project**: Searches only that project (returns empty if project doesn't exist)
- **Multiple projects**: Searches only existing projects, warns about non-existent ones
- **Empty projects list**: Treated as search all projects
- **Error Responses**:
```json
// Invalid sort_by parameter
{
"detail": "Invalid sort_by parameter: 'invalid_field'. Valid options are: ['match_score', 'name', 'type']"
}

// Invalid sort_order parameter
{
"detail": "Invalid sort_order parameter: 'invalid_order'. Valid options are: ['asc', 'desc']"
}

Comment thread
aniketpalu marked this conversation as resolved.
// No existing projects found
Comment thread
aniketpalu marked this conversation as resolved.
Outdated
{
"results": [],
"pagination": { "total_count": 0 },
"query": "user",
"projects_searched": [],
"error": "No projects found"
}
```
---

## Registry Server Configuration: Recent Visit Logging
Expand Down
180 changes: 180 additions & 0 deletions sdk/python/feast/api/registry/rest/rest_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,119 @@ def get_sorting_params(
"sort_order": sort_order or "asc",
}

def validate_or_set_default_sorting_params(
sort_by_options: List[str] = [],
default_sort_by_option: str = "",
default_sort_order: str = "asc"
) -> Callable:
def set_input_or_default(
sort_by: Optional[str] = Query(None),
sort_order: Optional[str] = Query(None)
) -> dict:
sorting_params = {}
if not sort_by_options:
return {
"sort_by": default_sort_by_option,
"sort_order": default_sort_order
}

if sort_by:
if sort_by in sort_by_options:
sorting_params["sort_by"] = sort_by
else:
raise HTTPException(
status_code=400,
detail=f"Invalid sort_by parameter: '{sort_by}'. Valid options are: {sort_by_options}",
)
else:
sorting_params["sort_by"] = default_sort_by_option

if sort_order:
if sort_order in ["asc", "desc"]:
sorting_params["sort_order"] = sort_order
else:
raise HTTPException(
status_code=400,
detail=f"Invalid sort_order parameter: '{sort_order}'. Valid options are: ['asc', 'desc']",
)
else:
sorting_params["sort_order"] = default_sort_order

return sorting_params

return set_input_or_default

def validate_or_set_default_pagination_params(
default_page: int = 1,
default_limit: int = 50,
min_page: int = 1,
min_limit: int = 1,
max_limit: int = 100
) -> Callable:
"""
Factory function to create a FastAPI dependency for validating pagination parameters.

Args:
default_page: Default page number if not provided
default_limit: Default limit if not provided
min_page: Minimum allowed page number
min_limit: Minimum allowed limit
max_limit: Maximum allowed limit

Returns:
Callable that can be used as FastAPI dependency for pagination validation

Example usage:
# Create a custom pagination validator
custom_pagination = validate_or_set_default_pagination_params(
default_page=1,
default_limit=25,
max_limit=200
)

# Use in FastAPI route
@router.get("/items")
def get_items(pagination_params: dict = Depends(custom_pagination)):
page = pagination_params["page"]
limit = pagination_params["limit"]
# Use page and limit for your logic
"""
def set_input_or_default(
page: Optional[int] = Query(None),
limit: Optional[int] = Query(None)
) -> dict:
pagination_params = {}

# Validate and set page parameter
if page is not None:
if page < min_page:
raise HTTPException(
status_code=400,
detail=f"Invalid page parameter: '{page}'. Must be greater than or equal to {min_page}",
)
pagination_params["page"] = page
else:
pagination_params["page"] = default_page

# Validate and set limit parameter
if limit is not None:
if limit < min_limit:
raise HTTPException(
status_code=400,
detail=f"Invalid limit parameter: '{limit}'. Must be greater than or equal to {min_limit}",
)
if limit > max_limit:
raise HTTPException(
status_code=400,
detail=f"Invalid limit parameter: '{limit}'. Must be less than or equal to {max_limit}",
)
pagination_params["limit"] = limit
else:
pagination_params["limit"] = default_limit

return pagination_params

return set_input_or_default

def create_grpc_pagination_params(
pagination_params: dict,
Expand Down Expand Up @@ -361,3 +474,70 @@ def get_all_project_resources(
except Exception as e:
Comment thread
aniketpalu marked this conversation as resolved.
logger.error(f"Error getting resources for project '{project}': {e}")
return resources # Return empty resources dict on error


def filter_search_results_and_match_score(
results: List[Dict], query: str
) -> List[Dict]:
"""Filter search results based on query string"""
if not query:
return results

query_lower = query.lower()
filtered_results = []

for result in results:
# Search in name
if query_lower in result.get("name", "").lower():
result["match_score"] = 100 # Exact name match gets highest score
Comment thread
aniketpalu marked this conversation as resolved.
Outdated
filtered_results.append(result)
continue

# Search in description
if query_lower in result.get("description", "").lower():
result["match_score"] = 80
filtered_results.append(result)
continue

# Search in tags
tags = result.get("tags", {})
tag_match = False
for key, value in tags.items():
if query_lower in key.lower() or query_lower in str(value).lower():
tag_match = True
break

if tag_match:
result["match_score"] = 60
filtered_results.append(result)
continue

# Search in features (for feature views and services)
features = result.get("features", [])
feature_match = any(query_lower in feature.lower() for feature in features)

if feature_match:
result["match_score"] = 70
filtered_results.append(result)
continue

# Partial name match (fuzzy search)
if fuzzy_match(query_lower, result.get("name", "").lower()):
result["match_score"] = 40
filtered_results.append(result)

return filtered_results


def fuzzy_match(query: str, text: str, threshold: float = 0.6) -> bool:
"""Simple fuzzy matching using character overlap"""
if not query or not text:
return False

query_chars = set(query)
text_chars = set(text)

overlap = len(query_chars.intersection(text_chars))
similarity = overlap / len(query_chars.union(text_chars))

return similarity >= threshold
Comment thread
aniketpalu marked this conversation as resolved.
Outdated
Loading