Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
151 changes: 151 additions & 0 deletions docs/reference/feature-servers/registry-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,157 @@ 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. Empty string returns all resources.
- `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 (starts from 1)
- `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")
- **Search Algorithm**:
- **Exact name match**: Highest priority (score: 100)
- **Description match**: High priority (score: 80)
- **Feature name match**: Medium-high priority (score: 50)
- **Tag match**: Medium priority (score: 60)
- **Fuzzy name match**: Lower priority (score: 40, similarity threshold: 50%)
- **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,
"totalCount": 4,
"totalPages": 1,
"hasNext": false,
"hasPrevious": false
},
"errors": []
}
```
- **Project Handling**:
- **No projects specified**: Searches all available projects
- **Single project**: Searches only that project (includes warning if project doesn't exist)
- **Multiple projects**: Searches only existing projects, includes warnings about non-existent ones
- **Empty projects list**: Treated as search all projects
- **Error Responses**:
```json
// Invalid sort_by parameter (HTTP 400)
{
"detail": "Invalid sort_by parameter: 'invalid_field'. Valid options are: ['match_score', 'name', 'type']"
}

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

// Invalid pagination limit above maximum (HTTP 400)
{
"detail": "Invalid limit parameter: '150'. Must be less than or equal to 100"
}

// Missing required query parameter (HTTP 422)
{
"detail": [
{
"type": "missing",
"loc": ["query_params", "query"],
"msg": "Field required"
}
]
}

// Successful response with warnings
{
"query": "user",
"projects_searched": ["existing_project"],
"results": [],
"pagination": {
"page": 1,
"limit": 50,
"totalCount": 0,
"totalPages": 0
},
"errors": ["Following projects do not exist: nonexistent_project"]
}

// Successful response but empty results
{
"query": "user",
"projects_searched": ["existing_project"],
"results": [],
"pagination": {
"page": 1,
"limit": 50,
"totalCount": 0,
"totalPages": 0
},
"errors": []
}
```
---
#### Get Popular Tags
- **Endpoint**: `GET /api/v1/metrics/popular_tags`
- **Description**: Discover Feature Views by popular tags. Returns the most popular tags (tags assigned to maximum number of feature views) with their associated feature views. If no project is specified, returns popular tags across all projects.
Expand Down
2 changes: 2 additions & 0 deletions sdk/python/feast/api/registry/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from feast.api.registry.rest.permissions import get_permission_router
from feast.api.registry.rest.projects import get_project_router
from feast.api.registry.rest.saved_datasets import get_saved_dataset_router
from feast.api.registry.rest.search import get_search_router


def register_all_routes(app: FastAPI, grpc_handler, server=None):
Expand All @@ -22,4 +23,5 @@ def register_all_routes(app: FastAPI, grpc_handler, server=None):
app.include_router(get_permission_router(grpc_handler))
app.include_router(get_project_router(grpc_handler))
app.include_router(get_saved_dataset_router(grpc_handler))
app.include_router(get_search_router(grpc_handler))
app.include_router(get_metrics_router(grpc_handler, server))
150 changes: 53 additions & 97 deletions sdk/python/feast/api/registry/rest/lineage.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
"""REST API endpoints for registry lineage and relationships."""

import logging
from typing import Optional

from fastapi import APIRouter, Depends, Query

from feast.api.registry.rest.rest_utils import (
create_grpc_pagination_params,
create_grpc_sorting_params,
get_all_project_resources,
get_pagination_params,
get_sorting_params,
grpc_call,
)
from feast.protos.feast.registry import RegistryServer_pb2

logger = logging.getLogger(__name__)


def get_lineage_router(grpc_handler) -> APIRouter:
router = APIRouter()
Expand Down Expand Up @@ -141,69 +145,44 @@ def get_complete_registry_data(
)
lineage_response = grpc_call(grpc_handler.GetRegistryLineage, lineage_req)

# Get all registry objects
entities_req = RegistryServer_pb2.ListEntitiesRequest(
project=project,
allow_cache=allow_cache,
pagination=grpc_pagination,
sorting=grpc_sorting,
)
entities_response = grpc_call(grpc_handler.ListEntities, entities_req)

data_sources_req = RegistryServer_pb2.ListDataSourcesRequest(
project=project,
allow_cache=allow_cache,
pagination=grpc_pagination,
sorting=grpc_sorting,
)
data_sources_response = grpc_call(
grpc_handler.ListDataSources, data_sources_req
)

feature_views_req = RegistryServer_pb2.ListAllFeatureViewsRequest(
project=project,
allow_cache=allow_cache,
pagination=grpc_pagination,
sorting=grpc_sorting,
)
feature_views_response = grpc_call(
grpc_handler.ListAllFeatureViews, feature_views_req
)

feature_services_req = RegistryServer_pb2.ListFeatureServicesRequest(
project=project,
allow_cache=allow_cache,
pagination=grpc_pagination,
sorting=grpc_sorting,
)
feature_services_response = grpc_call(
grpc_handler.ListFeatureServices, feature_services_req
)

features_req = RegistryServer_pb2.ListFeaturesRequest(
project=project,
pagination=grpc_pagination,
sorting=grpc_sorting,
# Get all registry objects using shared helper function
project_resources, pagination, errors = get_all_project_resources(
grpc_handler,
project,
allow_cache,
tags={},
pagination_params=pagination_params,
sorting_params=sorting_params,
)
features_response = grpc_call(grpc_handler.ListFeatures, features_req)

if errors and not project_resources:
logger.error(
f"Error getting project resources for project {project}: {errors}"
)
return {
"project": project,
"objects": {},
"relationships": [],
"indirectRelationships": [],
"pagination": {},
}
return {
"project": project,
"objects": {
"entities": entities_response.get("entities", []),
"dataSources": data_sources_response.get("dataSources", []),
"featureViews": feature_views_response.get("featureViews", []),
"featureServices": feature_services_response.get("featureServices", []),
"features": features_response.get("features", []),
"entities": project_resources.get("entities", []),
"dataSources": project_resources.get("dataSources", []),
"featureViews": project_resources.get("featureViews", []),
"featureServices": project_resources.get("featureServices", []),
"features": project_resources.get("features", []),
},
"relationships": lineage_response.get("relationships", []),
"indirectRelationships": lineage_response.get("indirectRelationships", []),
"pagination": {
"entities": entities_response.get("pagination", {}),
"dataSources": data_sources_response.get("pagination", {}),
"featureViews": feature_views_response.get("pagination", {}),
"featureServices": feature_services_response.get("pagination", {}),
"features": features_response.get("pagination", {}),
# Get pagination metadata from project_resources if available, otherwise use empty dicts
"entities": pagination.get("entities", {}),
"dataSources": pagination.get("dataSources", {}),
"featureViews": pagination.get("featureViews", {}),
"featureServices": pagination.get("featureServices", {}),
"features": pagination.get("features", {}),
"relationships": lineage_response.get("relationshipsPagination", {}),
"indirectRelationships": lineage_response.get(
"indirectRelationshipsPagination", {}
Expand Down Expand Up @@ -265,61 +244,38 @@ def get_complete_registry_data_all(
allow_cache=allow_cache,
)
lineage_response = grpc_call(grpc_handler.GetRegistryLineage, lineage_req)
# Get all registry objects
entities_req = RegistryServer_pb2.ListEntitiesRequest(
project=project_name,
allow_cache=allow_cache,
)
entities_response = grpc_call(grpc_handler.ListEntities, entities_req)
data_sources_req = RegistryServer_pb2.ListDataSourcesRequest(
project=project_name,
allow_cache=allow_cache,
)
data_sources_response = grpc_call(
grpc_handler.ListDataSources, data_sources_req
)
feature_views_req = RegistryServer_pb2.ListAllFeatureViewsRequest(
project=project_name,
allow_cache=allow_cache,
)
feature_views_response = grpc_call(
grpc_handler.ListAllFeatureViews, feature_views_req
)
feature_services_req = RegistryServer_pb2.ListFeatureServicesRequest(
project=project_name,
allow_cache=allow_cache,
)
feature_services_response = grpc_call(
grpc_handler.ListFeatureServices, feature_services_req
)

features_req = RegistryServer_pb2.ListFeaturesRequest(
project=project_name,
# Get all registry objects using shared helper function
project_resources, _, errors = get_all_project_resources(
grpc_handler, project_name, allow_cache, tags={}
)
features_response = grpc_call(grpc_handler.ListFeatures, features_req)

if errors and not project_resources:
logger.error(
f"Error getting project resources for project {project_name}: {errors}"
)
continue

# Add project field to each object
for entity in entities_response.get("entities", []):
for entity in project_resources.get("entities", []):
entity["project"] = project_name
for ds in data_sources_response.get("dataSources", []):
for ds in project_resources.get("dataSources", []):
ds["project"] = project_name
for fv in feature_views_response.get("featureViews", []):
for fv in project_resources.get("featureViews", []):
fv["project"] = project_name
for fs in feature_services_response.get("featureServices", []):
for fs in project_resources.get("featureServices", []):
fs["project"] = project_name
for feat in features_response.get("features", []):
for feat in project_resources.get("features", []):
feat["project"] = project_name
all_data.append(
{
"project": project_name,
"objects": {
"entities": entities_response.get("entities", []),
"dataSources": data_sources_response.get("dataSources", []),
"featureViews": feature_views_response.get("featureViews", []),
"featureServices": feature_services_response.get(
"featureServices", []
),
"features": features_response.get("features", []),
"entities": project_resources.get("entities", []),
"dataSources": project_resources.get("dataSources", []),
"featureViews": project_resources.get("featureViews", []),
"featureServices": project_resources.get("featureServices", []),
"features": project_resources.get("features", []),
},
"relationships": lineage_response.get("relationships", []),
"indirectRelationships": lineage_response.get(
Expand Down
Loading
Loading