Visitar URL original
feat: Making feast vector store with open ai search api compatible by patelchaitany · Pull Request #6121 · feast-dev/feast · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
docs: Update filtering requirements for Postgres and SQLite backends …
…in alpha-vector-database.md; enhance error handling in feature_server.py for invalid requests; clarify unsupported parameters in feature_store.py; ensure requested_features is a list in remote.py

Signed-off-by: Chaitany patel <patelchaitany93@gmail.com>
Signed-off-by: Chaitany Patel <patelchaitany93@gmail.com>
  • Loading branch information
patelchaitany committed Jul 21, 2026
commit ec4ccceefce1bc613bd129535735264f2f58abcc
2 changes: 1 addition & 1 deletion docs/reference/alpha-vector-database.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ The endpoint supports OpenAI-style filters for narrowing results beyond vector s
}
```

String equality filters work on all backends. Numeric and boolean filters require `enable_openai_compatible_store: true` in the online store config.
For Postgres and SQLite backends, all filtering (including string equality) requires `enable_openai_compatible_store: true` in the online store config. After enabling, run `feast apply` to update the database schema.

### Response format

Expand Down
18 changes: 18 additions & 0 deletions sdk/python/feast/feature_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,14 @@ async def vector_store_search(
"/v1/vector_stores/{vector_store_id}/search"
):
try:
feature_view = await run_in_threadpool(
store.get_feature_view, vector_store_id
)
assert_permissions(
resource=feature_view,
actions=[AuthzedAction.READ_ONLINE],
)

result = await store.retrieve_online_documents_openai(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think there's a bug here: this new endpoint bypasses _get_features, so auth-enabled servers never run the existing READ_ONLINE check for the target feature view. /search goes through _get_features() before retrieval, but this path calls store.retrieve_online_documents_openai(...) directly. Can we resolve the feature view, assert AuthzedAction.READ_ONLINE, and add an auth regression test for this endpoint?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I will add those test as well as the authentication checks for this newly added api endpoints

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auth regression test is out of scope for this PR, will address in a follow-up

vector_store_id=vector_store_id,
query=request.query,
Expand Down Expand Up @@ -582,6 +590,16 @@ async def vector_store_search(
}
},
)
except ValueError as e:
return JSONResponse(
status_code=422,
content={
"error": {
"message": str(e),
"type": "invalid_request_error",
}
},
Comment on lines +644 to +649
)
return JSONResponse(content=result)

@app.post("/push", dependencies=[Depends(inject_user_details)])
Expand Down
28 changes: 23 additions & 5 deletions sdk/python/feast/feature_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -4169,10 +4169,11 @@ async def retrieve_online_documents_openai(
query: Natural language query string, or list of strings.
max_num_results: Maximum number of results to return.
filters: OpenAI-compatible filters applied to the search.
ranking_options: OpenAI-compatible ranking options (accepted
but not yet applied).
rewrite_query: Whether to rewrite the query (accepted but
not yet applied).
ranking_options: OpenAI-compatible ranking options. Currently
unsupported; a ``ValueError`` is raised if
``score_threshold`` or ``ranker`` are provided.
rewrite_query: Whether to rewrite the query. Currently
unsupported; a ``ValueError`` is raised if set.
features_to_retrieve: Specific feature names to return.
If None, all features from the feature view are used.

Expand Down Expand Up @@ -4201,12 +4202,29 @@ async def retrieve_online_documents_openai(
features_to_retrieve=["name", "description"],
)
"""
unsupported: List[str] = []
if ranking_options:
if ranking_options.get("score_threshold") is not None:
unsupported.append("ranking_options.score_threshold")
if ranking_options.get("ranker") is not None:
unsupported.append("ranking_options.ranker")
if rewrite_query is not None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this rejects rewrite_query: false, even though that is the no-op/default shape an OpenAI-compatible client can send. OGX keeps this field for API compatibility and ignores false at the store layer. Can we only reject rewrite_query=True until query rewriting is implemented?

unsupported.append("rewrite_query")
if unsupported:
raise ValueError(
f"The following parameters are not yet supported: "
f"{', '.join(unsupported)}. Remove them from the request or "
f"wait for a future release that implements them."
)

feature_view = self.get_feature_view(vector_store_id)

if features_to_retrieve:
feature_names = features_to_retrieve
else:
feature_names = [f.name for f in feature_view.features]
feature_names = [
f.name for f in feature_view.features if not f.vector_index
]

features = [f"{feature_view.name}:{name}" for name in feature_names]
query_text = query if isinstance(query, str) else " ".join(query)
Expand Down
Comment thread
patelchaitany marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from_feast_type,
)
from feast.utils import (
_distance_to_score,
_serialize_vector_to_float_list,
to_naive_utc,
)
Expand Down Expand Up @@ -463,9 +464,13 @@ def online_write_batch(
single_entity_record.update(values_dict)
for field in required_fields:
if field not in single_entity_record:
single_entity_record[field] = _default_for_milvus_type(
collection_field_types.get(field, DataType.VARCHAR)
)
field_type = collection_field_types.get(field, DataType.VARCHAR)
if field == "_placeholder_vector":
single_entity_record[field] = [float("nan")]
else:
single_entity_record[field] = _default_for_milvus_type(
field_type
)
# Store only the latest event timestamp per entity
if (
entity_key_str not in unique_entities
Expand Down Expand Up @@ -943,9 +948,15 @@ def _combine_exprs(*parts: Optional[str]) -> Optional[str]:
else:
val.string_val = str(field_value)
res[field] = val
distance = hit.get("distance", None)
raw_distance = hit.get("distance", None)
res["distance"] = (
ValueProto(float_val=distance) if distance else ValueProto()
ValueProto(
float_val=_distance_to_score(raw_distance, distance_metric)
)
if raw_distance is not None and embedding is not None
else ValueProto(float_val=raw_distance)
if raw_distance is not None
else ValueProto()
)
result_list.append((res_ts, entity_key_proto, res if res else None))
return result_list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto
from feast.protos.feast.types.Value_pb2 import Value as ValueProto
from feast.repo_config import RepoConfig
from feast.utils import _build_retrieve_online_document_record
from feast.utils import _build_retrieve_online_document_record, _distance_to_score

SUPPORTED_DISTANCE_METRICS_DICT = {
"cosine": "<=>",
Expand Down Expand Up @@ -1044,7 +1044,9 @@ def retrieve_online_documents_v2(
"vector_distance"
] != float("inf"):
dist_val = ValueProto()
dist_val.double_val = entity_data["vector_distance"]
dist_val.double_val = _distance_to_score(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this fix landed in the wrong layer. retrieve_online_documents_v2() and /search still expose this synthetic field as distance, but Postgres now stores a higher-is-better OpenAI score in it. That changes the native Feast contract for non-OpenAI callers, and the same pattern appears in SQLite/Milvus. Can we keep the stores returning raw distances and convert to OpenAI score only in retrieve_online_documents_openai()?

entity_data["vector_distance"], distance_metric
)
features["distance"] = dist_val

if embedding is None or query_string is not None:
Expand Down
2 changes: 2 additions & 0 deletions sdk/python/feast/infra/online_stores/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,8 @@ def retrieve_online_documents_v2(
result_tuples = []
if requested_features is None:
requested_features = []
else:
requested_features = list(requested_features)
if "distance" not in requested_features:
requested_features.append("distance")
Comment thread
patelchaitany marked this conversation as resolved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still mutates the caller-owned requested_features list. When this is called through FeatureStore._retrieve_from_online_store_v2, Feast later builds features_to_request = requested_features + ["distance"], so distance can appear twice in the response metadata. Please copy before appending, or keep synthetic fields owned by one layer only.

for row_idx in range(num_results):
Expand Down
7 changes: 6 additions & 1 deletion sdk/python/feast/infra/online_stores/sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
from feast.types import FEAST_VECTOR_TYPES, PrimitiveFeastType
from feast.utils import (
_build_retrieve_online_document_record,
_distance_to_score,
_get_feature_view_vector_field_metadata,
_serialize_vector_to_float_list,
to_naive_utc,
Expand Down Expand Up @@ -873,7 +874,11 @@ def retrieve_online_documents_v2(
entity_dict[entity_key][vector_field] = _serialize_vector_to_float_list(
vector_value
)
entity_dict[entity_key]["distance"] = ValueProto(float_val=distance)
entity_dict[entity_key]["distance"] = ValueProto(
float_val=_distance_to_score(distance, distance_metric)
if embedding is not None
else distance
)
entity_dict[entity_key]["event_ts"] = event_ts
entity_dict[entity_key]["created_ts"] = created_ts

Expand Down
21 changes: 21 additions & 0 deletions sdk/python/feast/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1779,3 +1779,24 @@ def _get_feature_view_vector_field_metadata(
if not vector_fields:
return None
return vector_fields[0]


def _distance_to_score(distance: float, metric: Optional[str] = None) -> float:
"""Convert a raw distance value into a higher-is-better relevance score.

OpenAI clients treat ``score`` as higher-is-better (used with
``score_threshold``). Online stores return raw *distance* values where
lower is better, so a metric-aware conversion is required.

* **L2 / euclidean**: ``1 / (1 + distance)`` — maps [0, inf) to (0, 1]
* **cosine** (pgvector ``<=>`` returns ``1 - cosine_similarity``):
``1 - distance`` — recovers cosine similarity, clamped >= 0
* **inner_product / ip / dot** (pgvector ``<#>`` returns ``-dot(a, b)``):
``-distance`` — recovers the raw inner product
"""
m = (metric or "L2").lower()
if m in ("cosine",):
return max(0.0, 1.0 - distance)
if m in ("inner_product", "ip", "dot"):
return -distance
return 1.0 / (1.0 + distance)