Repository: netbox-community/netbox

Issue:
Enable caching of object attributes in search index
### NetBox version

v3.7.3

### Feature type

New functionality

### Proposed functionality

Extend `SearchIndex` to enable caching object attributes in addition to concrete model fields. This can be achieved by tweaking its `get_field_type()` method to also accept attributes. (`get_field_value()` probably doesn't need modification as it's simply wrapping `getattr()`.)

### Use case

This will enable NetBox to capture rendered attributes in addition to raw database values. For instance, #14201 requests caching the number of an ASN object as the string "AS1234" in addition to the plain numeric value.

### Database changes

None expected

### External dependencies

None


Discussion:

Resolving patch:
diff --git a/netbox/netbox/search/__init__.py b/netbox/netbox/search/__init__.py
--- a/netbox/netbox/search/__init__.py
+++ b/netbox/netbox/search/__init__.py
@@ -1,6 +1,9 @@
 from collections import namedtuple
+from decimal import Decimal
 
+from django.core.exceptions import FieldDoesNotExist
 from django.db import models
+from netaddr import IPAddress, IPNetwork
 
 from ipam.fields import IPAddressField, IPNetworkField
 from netbox.registry import registry
@@ -56,6 +59,24 @@ def get_field_type(instance, field_name):
             return FieldTypes.INTEGER
         return FieldTypes.STRING
 
+    @staticmethod
+    def get_attr_type(instance, field_name):
+        """
+        Return the data type of the specified object attribute.
+        """
+        value = getattr(instance, field_name)
+        if type(value) is str:
+            return FieldTypes.STRING
+        if type(value) is int:
+            return FieldTypes.INTEGER
+        if type(value) in (float, Decimal):
+            return FieldTypes.FLOAT
+        if type(value) is IPNetwork:
+            return FieldTypes.CIDR
+        if type(value) is IPAddress:
+            return FieldTypes.INET
+        return FieldTypes.STRING
+
     @staticmethod
     def get_field_value(instance, field_name):
         """
@@ -82,7 +103,11 @@ def to_cache(cls, instance, custom_fields=None):
 
         # Capture built-in fields
         for name, weight in cls.fields:
-            type_ = cls.get_field_type(instance, name)
+            try:
+                type_ = cls.get_field_type(instance, name)
+            except FieldDoesNotExist:
+                # Not a concrete field; handle as an object attribute
+                type_ = cls.get_attr_type(instance, name)
             value = cls.get_field_value(instance, name)
             if type_ and value:
                 values.append(
diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py
--- a/netbox/netbox/tables/tables.py
+++ b/netbox/netbox/tables/tables.py
@@ -263,9 +263,11 @@ def __init__(self, data, highlight=None, **kwargs):
         super().__init__(data, **kwargs)
 
     def render_field(self, value, record):
-        if hasattr(record.object, value):
-            return title(record.object._meta.get_field(value).verbose_name)
-        return value
+        try:
+            model_field = record.object._meta.get_field(value)
+            return title(model_field.verbose_name)
+        except FieldDoesNotExist:
+            return value
 
     def render_value(self, value):
         if not self.highlight:


Test patch: