diff --git a/docs/development/search.md b/docs/development/search.md index 27730a6f5..02bcaa898 100644 --- a/docs/development/search.md +++ b/docs/development/search.md @@ -1,6 +1,27 @@ # Search -## Field Weight Guidance +NetBox v3.4 introduced a new global search mechanism, which employs the `extras.CachedValue` model to store discrete field values from many models in a single table. + +## SearchIndex + +To enable search support for a model, declare and register a subclass of `netbox.search.SearchIndex` for it. Typically, this will be done within an app's `search.py` module. + +```python +from netbox.search import SearchIndex, register_search + +@register_search +class MyModelIndex(SearchIndex): + model = MyModel + fields = ( + ('name', 100), + ('description', 500), + ('comments', 5000), + ) +``` + +A SearchIndex subclass defines both its model and a list of two-tuples specifying which model fields to be indexed and the weight (precedence) associated with each. Guidance on weight assignment for fields is provided below. + +### Field Weight Guidance | Weight | Field Role | Examples | |--------|--------------------------------------------------|----------------------------------------------------| diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 8a9c61e22..80b94d6c2 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -11,6 +11,10 @@ ### New Features +#### New Global Search ([#10560](https://github.com/netbox-community/netbox/issues/10560)) + +NetBox's global search functionality has been completely overhauled and replaced by a new cache-based lookup. + #### Top-Level Plugin Navigation Menus ([#9071](https://github.com/netbox-community/netbox/issues/9071)) A new `PluginMenu` class has been introduced, which enables a plugin to inject a top-level menu in NetBox's navigation menu. This menu can have one or more groups of menu items, just like core items. Backward compatibility with the existing `menu_items` has been maintained. diff --git a/netbox/circuits/search.py b/netbox/circuits/search.py index 2e4191420..673f6308f 100644 --- a/netbox/circuits/search.py +++ b/netbox/circuits/search.py @@ -2,7 +2,7 @@ from netbox.search import SearchIndex, register_search from . import models -@register_search() +@register_search class CircuitIndex(SearchIndex): model = models.Circuit fields = ( @@ -12,7 +12,7 @@ class CircuitIndex(SearchIndex): ) -@register_search() +@register_search class CircuitTerminationIndex(SearchIndex): model = models.CircuitTermination fields = ( @@ -24,7 +24,7 @@ class CircuitTerminationIndex(SearchIndex): ) -@register_search() +@register_search class CircuitTypeIndex(SearchIndex): model = models.CircuitType fields = ( @@ -34,7 +34,7 @@ class CircuitTypeIndex(SearchIndex): ) -@register_search() +@register_search class ProviderIndex(SearchIndex): model = models.Provider fields = ( @@ -44,7 +44,7 @@ class ProviderIndex(SearchIndex): ) -@register_search() +@register_search class ProviderNetworkIndex(SearchIndex): model = models.ProviderNetwork fields = ( diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py index bd0effb12..d34a78888 100644 --- a/netbox/dcim/search.py +++ b/netbox/dcim/search.py @@ -2,7 +2,7 @@ from netbox.search import SearchIndex, register_search from . import models -@register_search() +@register_search class CableIndex(SearchIndex): model = models.Cable fields = ( @@ -10,7 +10,7 @@ class CableIndex(SearchIndex): ) -@register_search() +@register_search class ConsolePortIndex(SearchIndex): model = models.ConsolePort fields = ( @@ -21,7 +21,7 @@ class ConsolePortIndex(SearchIndex): ) -@register_search() +@register_search class ConsoleServerPortIndex(SearchIndex): model = models.ConsoleServerPort fields = ( @@ -32,7 +32,7 @@ class ConsoleServerPortIndex(SearchIndex): ) -@register_search() +@register_search class DeviceIndex(SearchIndex): model = models.Device fields = ( @@ -43,7 +43,7 @@ class DeviceIndex(SearchIndex): ) -@register_search() +@register_search class DeviceBayIndex(SearchIndex): model = models.DeviceBay fields = ( @@ -53,7 +53,7 @@ class DeviceBayIndex(SearchIndex): ) -@register_search() +@register_search class DeviceRoleIndex(SearchIndex): model = models.DeviceRole fields = ( @@ -63,7 +63,7 @@ class DeviceRoleIndex(SearchIndex): ) -@register_search() +@register_search class DeviceTypeIndex(SearchIndex): model = models.DeviceType fields = ( @@ -73,7 +73,7 @@ class DeviceTypeIndex(SearchIndex): ) -@register_search() +@register_search class FrontPortIndex(SearchIndex): model = models.FrontPort fields = ( @@ -83,7 +83,7 @@ class FrontPortIndex(SearchIndex): ) -@register_search() +@register_search class InterfaceIndex(SearchIndex): model = models.Interface fields = ( @@ -97,7 +97,7 @@ class InterfaceIndex(SearchIndex): ) -@register_search() +@register_search class InventoryItemIndex(SearchIndex): model = models.InventoryItem fields = ( @@ -110,7 +110,7 @@ class InventoryItemIndex(SearchIndex): ) -@register_search() +@register_search class LocationIndex(SearchIndex): model = models.Location fields = ( @@ -120,7 +120,7 @@ class LocationIndex(SearchIndex): ) -@register_search() +@register_search class ManufacturerIndex(SearchIndex): model = models.Manufacturer fields = ( @@ -130,7 +130,7 @@ class ManufacturerIndex(SearchIndex): ) -@register_search() +@register_search class ModuleIndex(SearchIndex): model = models.Module fields = ( @@ -140,7 +140,7 @@ class ModuleIndex(SearchIndex): ) -@register_search() +@register_search class ModuleBayIndex(SearchIndex): model = models.ModuleBay fields = ( @@ -150,7 +150,7 @@ class ModuleBayIndex(SearchIndex): ) -@register_search() +@register_search class ModuleTypeIndex(SearchIndex): model = models.ModuleType fields = ( @@ -160,7 +160,7 @@ class ModuleTypeIndex(SearchIndex): ) -@register_search() +@register_search class PlatformIndex(SearchIndex): model = models.Platform fields = ( @@ -171,7 +171,7 @@ class PlatformIndex(SearchIndex): ) -@register_search() +@register_search class PowerFeedIndex(SearchIndex): model = models.PowerFeed fields = ( @@ -180,7 +180,7 @@ class PowerFeedIndex(SearchIndex): ) -@register_search() +@register_search class PowerOutletIndex(SearchIndex): model = models.PowerOutlet fields = ( @@ -190,7 +190,7 @@ class PowerOutletIndex(SearchIndex): ) -@register_search() +@register_search class PowerPanelIndex(SearchIndex): model = models.PowerPanel fields = ( @@ -198,7 +198,7 @@ class PowerPanelIndex(SearchIndex): ) -@register_search() +@register_search class PowerPortIndex(SearchIndex): model = models.PowerPort fields = ( @@ -210,7 +210,7 @@ class PowerPortIndex(SearchIndex): ) -@register_search() +@register_search class RackIndex(SearchIndex): model = models.Rack fields = ( @@ -222,7 +222,7 @@ class RackIndex(SearchIndex): ) -@register_search() +@register_search class RackReservationIndex(SearchIndex): model = models.RackReservation fields = ( @@ -230,7 +230,7 @@ class RackReservationIndex(SearchIndex): ) -@register_search() +@register_search class RackRoleIndex(SearchIndex): model = models.RackRole fields = ( @@ -240,7 +240,7 @@ class RackRoleIndex(SearchIndex): ) -@register_search() +@register_search class RearPortIndex(SearchIndex): model = models.RearPort fields = ( @@ -250,7 +250,7 @@ class RearPortIndex(SearchIndex): ) -@register_search() +@register_search class RegionIndex(SearchIndex): model = models.Region fields = ( @@ -260,7 +260,7 @@ class RegionIndex(SearchIndex): ) -@register_search() +@register_search class SiteIndex(SearchIndex): model = models.Site fields = ( @@ -274,7 +274,7 @@ class SiteIndex(SearchIndex): ) -@register_search() +@register_search class SiteGroupIndex(SearchIndex): model = models.SiteGroup fields = ( @@ -284,7 +284,7 @@ class SiteGroupIndex(SearchIndex): ) -@register_search() +@register_search class VirtualChassisIndex(SearchIndex): model = models.VirtualChassis fields = ( diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index ea88c0f51..f855027e2 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -75,7 +75,7 @@ class PluginConfig(AppConfig): try: search_indexes = import_string(f"{self.__module__}.{self.search_indexes}") for idx in search_indexes: - register_search()(idx) + register_search(idx) except ImportError: pass diff --git a/netbox/extras/search.py b/netbox/extras/search.py index 9ae9f3365..da4aa1c84 100644 --- a/netbox/extras/search.py +++ b/netbox/extras/search.py @@ -2,7 +2,7 @@ from netbox.search import SearchIndex, register_search from . import models -@register_search() +@register_search class JournalEntryIndex(SearchIndex): model = models.JournalEntry fields = ( diff --git a/netbox/ipam/search.py b/netbox/ipam/search.py index f5a66fac9..d1d25da76 100644 --- a/netbox/ipam/search.py +++ b/netbox/ipam/search.py @@ -2,7 +2,7 @@ from . import models from netbox.search import SearchIndex, register_search -@register_search() +@register_search class AggregateIndex(SearchIndex): model = models.Aggregate fields = ( @@ -12,7 +12,7 @@ class AggregateIndex(SearchIndex): ) -@register_search() +@register_search class ASNIndex(SearchIndex): model = models.ASN fields = ( @@ -21,7 +21,7 @@ class ASNIndex(SearchIndex): ) -@register_search() +@register_search class FHRPGroupIndex(SearchIndex): model = models.FHRPGroup fields = ( @@ -31,7 +31,7 @@ class FHRPGroupIndex(SearchIndex): ) -@register_search() +@register_search class IPAddressIndex(SearchIndex): model = models.IPAddress fields = ( @@ -41,7 +41,7 @@ class IPAddressIndex(SearchIndex): ) -@register_search() +@register_search class IPRangeIndex(SearchIndex): model = models.IPRange fields = ( @@ -51,7 +51,7 @@ class IPRangeIndex(SearchIndex): ) -@register_search() +@register_search class L2VPNIndex(SearchIndex): model = models.L2VPN fields = ( @@ -61,7 +61,7 @@ class L2VPNIndex(SearchIndex): ) -@register_search() +@register_search class PrefixIndex(SearchIndex): model = models.Prefix fields = ( @@ -70,7 +70,7 @@ class PrefixIndex(SearchIndex): ) -@register_search() +@register_search class RIRIndex(SearchIndex): model = models.RIR fields = ( @@ -80,7 +80,7 @@ class RIRIndex(SearchIndex): ) -@register_search() +@register_search class RoleIndex(SearchIndex): model = models.Role fields = ( @@ -90,7 +90,7 @@ class RoleIndex(SearchIndex): ) -@register_search() +@register_search class RouteTargetIndex(SearchIndex): model = models.RouteTarget fields = ( @@ -99,7 +99,7 @@ class RouteTargetIndex(SearchIndex): ) -@register_search() +@register_search class ServiceIndex(SearchIndex): model = models.Service fields = ( @@ -108,7 +108,7 @@ class ServiceIndex(SearchIndex): ) -@register_search() +@register_search class VLANIndex(SearchIndex): model = models.VLAN fields = ( @@ -118,7 +118,7 @@ class VLANIndex(SearchIndex): ) -@register_search() +@register_search class VLANGroupIndex(SearchIndex): model = models.VLANGroup fields = ( @@ -129,7 +129,7 @@ class VLANGroupIndex(SearchIndex): ) -@register_search() +@register_search class VRFIndex(SearchIndex): model = models.VRF fields = ( diff --git a/netbox/netbox/search/__init__.py b/netbox/netbox/search/__init__.py index b990d1b2d..5b14e9ee1 100644 --- a/netbox/netbox/search/__init__.py +++ b/netbox/netbox/search/__init__.py @@ -91,17 +91,14 @@ def get_indexer(model): return registry['search'][app_label][model_name] -def register_search(): +def register_search(cls): """ Decorator for registering a SearchIndex with a particular model. """ - def _wrapper(cls): - model = cls.model - app_label = model._meta.app_label - model_name = model._meta.model_name + model = cls.model + app_label = model._meta.app_label + model_name = model._meta.model_name - registry['search'][app_label][model_name] = cls + registry['search'][app_label][model_name] = cls - return cls - - return _wrapper + return cls diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index 7a0eb2781..59136ae83 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -7,6 +7,7 @@ from django.core.exceptions import ImproperlyConfigured from django.db.models import F, Window from django.db.models.functions import window from django.db.models.signals import post_delete, post_save +from django.utils.module_loading import import_string from extras.models import CachedValue, CustomField from extras.registry import registry @@ -50,7 +51,7 @@ class SearchBackend: return self._search_choice_options - def search(self, request, value, object_types=None, lookup=DEFAULT_LOOKUP_TYPE): + def search(self, value, user=None, object_types=None, lookup=DEFAULT_LOOKUP_TYPE): """ Search cached object representations for the given value. """ @@ -102,9 +103,7 @@ class SearchBackend: class CachedValueSearchBackend(SearchBackend): - def search(self, value, user=None, object_types=None, lookup=None): - if not lookup: - lookup = DEFAULT_LOOKUP_TYPE + def search(self, value, user=None, object_types=None, lookup=DEFAULT_LOOKUP_TYPE): # Define the search parameters params = { @@ -230,16 +229,13 @@ class CachedValueSearchBackend(SearchBackend): def get_backend(): - """Initializes and returns the configured search backend.""" - backend_name = settings.SEARCH_BACKEND - - # Load the backend class - backend_module_name, backend_cls_name = backend_name.rsplit('.', 1) - backend_module = import_module(backend_module_name) + """ + Initializes and returns the configured search backend. + """ try: - backend_cls = getattr(backend_module, backend_cls_name) + backend_cls = import_string(settings.SEARCH_BACKEND) except AttributeError: - raise ImproperlyConfigured(f"Could not find a class named {backend_module_name} in {backend_cls_name}") + raise ImproperlyConfigured(f"Failed to import configured SEARCH_BACKEND: {settings.SEARCH_BACKEND}") # Initialize and return the backend instance return backend_cls() diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 15d302d23..9b86b2ed3 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -222,7 +222,9 @@ class SearchTable(tables.Table): super().__init__(data, **kwargs) def render_field(self, value, record): - return bettertitle(record.object._meta.get_field(value).verbose_name) + if hasattr(record.object, value): + return bettertitle(record.object._meta.get_field(value).verbose_name) + return value def render_value(self, value): if not self.highlight: diff --git a/netbox/netbox/views/__init__.py b/netbox/netbox/views/__init__.py index 70861df61..0f35dab49 100644 --- a/netbox/netbox/views/__init__.py +++ b/netbox/netbox/views/__init__.py @@ -167,11 +167,12 @@ class SearchView(View): app_label, model_name = obj_type.split('.') object_types.append(ContentType.objects.get_by_natural_key(app_label, model_name)) + lookup = form.cleaned_data['lookup'] or LookupTypes.PARTIAL results = search_backend.search( form.cleaned_data['q'], user=request.user, object_types=object_types, - lookup=form.cleaned_data['lookup'] + lookup=lookup ) if form.cleaned_data['lookup'] != LookupTypes.EXACT: diff --git a/netbox/tenancy/search.py b/netbox/tenancy/search.py index 79ab81609..8cb3c4ccb 100644 --- a/netbox/tenancy/search.py +++ b/netbox/tenancy/search.py @@ -2,7 +2,7 @@ from netbox.search import SearchIndex, register_search from . import models -@register_search() +@register_search class ContactIndex(SearchIndex): model = models.Contact fields = ( @@ -16,7 +16,7 @@ class ContactIndex(SearchIndex): ) -@register_search() +@register_search class ContactGroupIndex(SearchIndex): model = models.ContactGroup fields = ( @@ -26,7 +26,7 @@ class ContactGroupIndex(SearchIndex): ) -@register_search() +@register_search class ContactRoleIndex(SearchIndex): model = models.ContactRole fields = ( @@ -36,7 +36,7 @@ class ContactRoleIndex(SearchIndex): ) -@register_search() +@register_search class TenantIndex(SearchIndex): model = models.Tenant fields = ( @@ -47,7 +47,7 @@ class TenantIndex(SearchIndex): ) -@register_search() +@register_search class TenantGroupIndex(SearchIndex): model = models.TenantGroup fields = ( diff --git a/netbox/virtualization/search.py b/netbox/virtualization/search.py index 1e81dc9ae..184bf7049 100644 --- a/netbox/virtualization/search.py +++ b/netbox/virtualization/search.py @@ -2,7 +2,7 @@ from netbox.search import SearchIndex, register_search from . import models -@register_search() +@register_search class ClusterIndex(SearchIndex): model = models.Cluster fields = ( @@ -11,7 +11,7 @@ class ClusterIndex(SearchIndex): ) -@register_search() +@register_search class ClusterGroupIndex(SearchIndex): model = models.ClusterGroup fields = ( @@ -21,7 +21,7 @@ class ClusterGroupIndex(SearchIndex): ) -@register_search() +@register_search class ClusterTypeIndex(SearchIndex): model = models.ClusterType fields = ( @@ -31,7 +31,7 @@ class ClusterTypeIndex(SearchIndex): ) -@register_search() +@register_search class VirtualMachineIndex(SearchIndex): model = models.VirtualMachine fields = ( @@ -40,7 +40,7 @@ class VirtualMachineIndex(SearchIndex): ) -@register_search() +@register_search class VMInterfaceIndex(SearchIndex): model = models.VMInterface fields = ( diff --git a/netbox/wireless/search.py b/netbox/wireless/search.py index 62520a33b..55ca2977c 100644 --- a/netbox/wireless/search.py +++ b/netbox/wireless/search.py @@ -2,7 +2,7 @@ from netbox.search import SearchIndex, register_search from . import models -@register_search() +@register_search class WirelessLANIndex(SearchIndex): model = models.WirelessLAN fields = ( @@ -12,7 +12,7 @@ class WirelessLANIndex(SearchIndex): ) -@register_search() +@register_search class WirelessLANGroupIndex(SearchIndex): model = models.WirelessLANGroup fields = ( @@ -22,7 +22,7 @@ class WirelessLANGroupIndex(SearchIndex): ) -@register_search() +@register_search class WirelessLinkIndex(SearchIndex): model = models.WirelessLink fields = (