Skip to content
18 changes: 18 additions & 0 deletions redisvl/query/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,24 @@ def _set_value(
self._value = val
self._operator = operator

def is_missing(self) -> "FilterExpression":
"""Create a filter expression for documents missing this field.

Returns:
FilterExpression: A filter expression that matches documents where the field is missing.

.. code-block:: python

from redisvl.query.filter import Tag, Text, Num, Geo, Timestamp

f = Tag("brand").is_missing()
f = Text("title").is_missing()
f = Num("price").is_missing()
f = Geo("location").is_missing()
f = Timestamp("created_at").is_missing()
"""
return FilterExpression(f"ismissing(@{self._field})")


def check_operator_misuse(func: Callable) -> Callable:
@wraps(func)
Expand Down
107 changes: 82 additions & 25 deletions redisvl/schema/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class BaseFieldAttributes(BaseModel):

sortable: bool = Field(default=False)
"""Enable faster result sorting on the field at runtime"""
index_missing: bool = Field(default=False)
"""Allow indexing and searching for missing values (documents without the field)"""


class TextFieldAttributes(BaseFieldAttributes):
Expand All @@ -74,6 +76,8 @@ class TextFieldAttributes(BaseFieldAttributes):
"""Keep a suffix trie with all terms which match the suffix to optimize certain queries"""
phonetic_matcher: Optional[str] = None
"""Used to perform phonetic matching during search"""
index_empty: bool = Field(default=False)
"""Allow indexing and searching for empty strings"""


class TagFieldAttributes(BaseFieldAttributes):
Expand All @@ -85,6 +89,8 @@ class TagFieldAttributes(BaseFieldAttributes):
"""Treat text as case sensitive or not. By default, tag characters are converted to lowercase"""
withsuffixtrie: bool = Field(default=False)
"""Keep a suffix trie with all terms which match the suffix to optimize certain queries"""
index_empty: bool = Field(default=False)
"""Allow indexing and searching for empty strings"""


class NumericFieldAttributes(BaseFieldAttributes):
Expand Down Expand Up @@ -112,6 +118,8 @@ class BaseVectorFieldAttributes(BaseModel):
"""The distance metric used to measure query relevance"""
initial_cap: Optional[int] = None
"""Initial vector capacity in the index affecting memory allocation size of the index"""
index_missing: bool = Field(default=False)
"""Allow indexing and searching for missing values (documents without the field)"""

@field_validator("algorithm", "datatype", "distance_metric", mode="before")
@classmethod
Expand All @@ -129,6 +137,8 @@ def field_data(self) -> Dict[str, Any]:
}
if self.initial_cap is not None: # Only include it if it's set
field_data["INITIAL_CAP"] = self.initial_cap
if self.index_missing: # Only include it if it's set
field_data["INDEXMISSING"] = True
return field_data


Expand Down Expand Up @@ -190,14 +200,30 @@ class TextField(BaseField):

def as_redis_field(self) -> RedisField:
name, as_name = self._handle_names()
return RedisTextField(
name,
as_name=as_name,
weight=self.attrs.weight, # type: ignore
no_stem=self.attrs.no_stem, # type: ignore
phonetic_matcher=self.attrs.phonetic_matcher, # type: ignore
sortable=self.attrs.sortable,
)
# Build arguments for RedisTextField
kwargs: Dict[str, Any] = {
"weight": self.attrs.weight, # type: ignore
"no_stem": self.attrs.no_stem, # type: ignore
"sortable": self.attrs.sortable,
}

# Only add as_name if it's not None
if as_name is not None:
kwargs["as_name"] = as_name

# Only add phonetic_matcher if it's not None
if self.attrs.phonetic_matcher is not None: # type: ignore
kwargs["phonetic_matcher"] = self.attrs.phonetic_matcher # type: ignore

# Add INDEXMISSING if enabled
if self.attrs.index_missing: # type: ignore
kwargs["index_missing"] = True

# Add INDEXEMPTY if enabled
if self.attrs.index_empty: # type: ignore
kwargs["index_empty"] = True

return RedisTextField(name, **kwargs)


class TagField(BaseField):
Expand All @@ -208,13 +234,26 @@ class TagField(BaseField):

def as_redis_field(self) -> RedisField:
name, as_name = self._handle_names()
return RedisTagField(
name,
as_name=as_name,
separator=self.attrs.separator, # type: ignore
case_sensitive=self.attrs.case_sensitive, # type: ignore
sortable=self.attrs.sortable,
)
# Build arguments for RedisTagField
kwargs: Dict[str, Any] = {
"separator": self.attrs.separator, # type: ignore
"case_sensitive": self.attrs.case_sensitive, # type: ignore
"sortable": self.attrs.sortable,
}

# Only add as_name if it's not None
if as_name is not None:
kwargs["as_name"] = as_name

# Add INDEXMISSING if enabled
if self.attrs.index_missing: # type: ignore
kwargs["index_missing"] = True

# Add INDEXEMPTY if enabled
if self.attrs.index_empty: # type: ignore
kwargs["index_empty"] = True

return RedisTagField(name, **kwargs)


class NumericField(BaseField):
Expand All @@ -225,11 +264,20 @@ class NumericField(BaseField):

def as_redis_field(self) -> RedisField:
name, as_name = self._handle_names()
return RedisNumericField(
name,
as_name=as_name,
sortable=self.attrs.sortable,
)
# Build arguments for RedisNumericField
kwargs: Dict[str, Any] = {
"sortable": self.attrs.sortable,
}

# Only add as_name if it's not None
if as_name is not None:
kwargs["as_name"] = as_name

# Add INDEXMISSING if enabled
if self.attrs.index_missing: # type: ignore
kwargs["index_missing"] = True

return RedisNumericField(name, **kwargs)


class GeoField(BaseField):
Expand All @@ -240,11 +288,20 @@ class GeoField(BaseField):

def as_redis_field(self) -> RedisField:
name, as_name = self._handle_names()
return RedisGeoField(
name,
as_name=as_name,
sortable=self.attrs.sortable,
)
# Build arguments for RedisGeoField
kwargs: Dict[str, Any] = {
"sortable": self.attrs.sortable,
}

# Only add as_name if it's not None
if as_name is not None:
kwargs["as_name"] = as_name

# Add INDEXMISSING if enabled
if self.attrs.index_missing: # type: ignore
kwargs["index_missing"] = True

return RedisGeoField(name, **kwargs)


class FlatVectorField(BaseField):
Expand Down
3 changes: 2 additions & 1 deletion redisvl/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ def serialize_item(item):
else:
return item

serialized_data = model.model_dump(exclude_none=True)
# Use exclude_defaults=False to preserve all field attributes including new ones
serialized_data = model.model_dump(exclude_none=True, exclude_defaults=False)
for key, value in serialized_data.items():
serialized_data[key] = serialize_item(value)
return serialized_data
Expand Down
Loading
Loading