From 80bf6e7b2c6947b8e4c0265ed4d5a62dbf45b386 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 10 Oct 2022 15:54:41 -0400 Subject: [PATCH] Initial work on new search backend --- netbox/circuits/search.py | 74 ++++-- netbox/dcim/search.py | 258 +++++++++++-------- netbox/extras/management/commands/reindex.py | 25 ++ netbox/extras/migrations/0079_search.py | 31 +++ netbox/extras/models/__init__.py | 2 + netbox/extras/models/search.py | 40 +++ netbox/extras/search.py | 5 +- netbox/extras/tests/dummy_plugin/search.py | 1 - netbox/ipam/search.py | 106 ++++---- netbox/netbox/forms/__init__.py | 6 +- netbox/netbox/models/features.py | 1 + netbox/netbox/search/__init__.py | 8 + netbox/netbox/search/backends.py | 76 ++++-- netbox/netbox/views/__init__.py | 6 +- netbox/templates/search.html | 59 ++--- netbox/tenancy/search.py | 33 ++- netbox/virtualization/search.py | 12 +- netbox/wireless/search.py | 14 +- 18 files changed, 502 insertions(+), 255 deletions(-) create mode 100644 netbox/extras/management/commands/reindex.py create mode 100644 netbox/extras/migrations/0079_search.py create mode 100644 netbox/extras/models/search.py diff --git a/netbox/circuits/search.py b/netbox/circuits/search.py index 5adfb97fb..cdb122cf9 100644 --- a/netbox/circuits/search.py +++ b/netbox/circuits/search.py @@ -1,34 +1,66 @@ -import circuits.filtersets -import circuits.tables -from circuits.models import Circuit, Provider, ProviderNetwork +from circuits import filtersets, models from netbox.search import SearchIndex, register_search from utilities.utils import count_related @register_search() -class ProviderIndex(SearchIndex): - model = Provider - queryset = Provider.objects.annotate(count_circuits=count_related(Circuit, 'provider')) - filterset = circuits.filtersets.ProviderFilterSet - table = circuits.tables.ProviderTable - url = 'circuits:provider_list' +class CircuitIndex(SearchIndex): + model = models.Circuit + fields = ( + ('cid', 100), + ('description', 500), + ('comments', 1000), + ) + queryset = models.Circuit.objects.prefetch_related( + 'type', 'provider', 'tenant', 'tenant__group', 'terminations__site' + ) + filterset = filtersets.CircuitFilterSet @register_search() -class CircuitIndex(SearchIndex): - model = Circuit - queryset = Circuit.objects.prefetch_related( - 'type', 'provider', 'tenant', 'tenant__group', 'terminations__site' +class CircuitTerminationIndex(SearchIndex): + model = models.CircuitTermination + fields = ( + ('xconnect_id', 300), + ('pp_info', 300), + ('description', 500), + ('port_speed', 2000), + ('upstream_speed', 2000), ) - filterset = circuits.filtersets.CircuitFilterSet - table = circuits.tables.CircuitTable - url = 'circuits:circuit_list' + + +@register_search() +class CircuitTypeIndex(SearchIndex): + model = models.CircuitType + fields = ( + ('name', 100), + ('slug', 100), + ('description', 500), + ) + + +@register_search() +class ProviderIndex(SearchIndex): + model = models.Provider + fields = ( + ('name', 100), + ('account', 200), + ('comments', 1000), + ) + queryset = models.Provider.objects.annotate( + count_circuits=count_related(models.Circuit, 'provider') + ) + filterset = filtersets.ProviderFilterSet @register_search() class ProviderNetworkIndex(SearchIndex): - model = ProviderNetwork - queryset = ProviderNetwork.objects.prefetch_related('provider') - filterset = circuits.filtersets.ProviderNetworkFilterSet - table = circuits.tables.ProviderNetworkTable - url = 'circuits:providernetwork_list' + model = models.ProviderNetwork + fields = ( + ('name', 100), + ('service_id', 200), + ('description', 500), + ('comments', 1000), + ) + queryset = models.ProviderNetwork.objects.prefetch_related('provider') + filterset = filtersets.ProviderNetworkFilterSet diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py index b179402ce..1216cb82b 100644 --- a/netbox/dcim/search.py +++ b/netbox/dcim/search.py @@ -1,81 +1,28 @@ -import dcim.filtersets -import dcim.tables -from dcim.models import ( - Cable, - Device, - DeviceType, - Location, - Module, - ModuleType, - PowerFeed, - Rack, - RackReservation, - Site, - VirtualChassis, -) +from dcim import filtersets, models from netbox.search import SearchIndex, register_search from utilities.utils import count_related @register_search() -class SiteIndex(SearchIndex): - model = Site - queryset = Site.objects.prefetch_related('region', 'tenant', 'tenant__group') - filterset = dcim.filtersets.SiteFilterSet - table = dcim.tables.SiteTable - url = 'dcim:site_list' - - -@register_search() -class RackIndex(SearchIndex): - model = Rack - queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate( - device_count=count_related(Device, 'rack') +class CableIndex(SearchIndex): + model = models.Cable + fields = ( + ('label', 100), ) - filterset = dcim.filtersets.RackFilterSet - table = dcim.tables.RackTable - url = 'dcim:rack_list' - - -@register_search() -class RackReservationIndex(SearchIndex): - model = RackReservation - queryset = RackReservation.objects.prefetch_related('rack', 'user') - filterset = dcim.filtersets.RackReservationFilterSet - table = dcim.tables.RackReservationTable - url = 'dcim:rackreservation_list' - - -@register_search() -class LocationIndex(SearchIndex): - model = Location - queryset = Location.objects.add_related_count( - Location.objects.add_related_count(Location.objects.all(), Device, 'location', 'device_count', cumulative=True), - Rack, - 'location', - 'rack_count', - cumulative=True, - ).prefetch_related('site') - filterset = dcim.filtersets.LocationFilterSet - table = dcim.tables.LocationTable - url = 'dcim:location_list' - - -@register_search() -class DeviceTypeIndex(SearchIndex): - model = DeviceType - queryset = DeviceType.objects.prefetch_related('manufacturer').annotate( - instance_count=count_related(Device, 'device_type') - ) - filterset = dcim.filtersets.DeviceTypeFilterSet - table = dcim.tables.DeviceTypeTable - url = 'dcim:devicetype_list' + queryset = models.Cable.objects.all() + filterset = filtersets.CableFilterSet @register_search() class DeviceIndex(SearchIndex): - model = Device - queryset = Device.objects.prefetch_related( + model = models.Device + fields = ( + ('asset_tag', 50), + ('serial', 60), + ('name', 100), + ('comments', 1000), + ) + queryset = models.Device.objects.prefetch_related( 'device_type__manufacturer', 'device_role', 'tenant', @@ -85,59 +32,162 @@ class DeviceIndex(SearchIndex): 'primary_ip4', 'primary_ip6', ) - filterset = dcim.filtersets.DeviceFilterSet - table = dcim.tables.DeviceTable - url = 'dcim:device_list' + filterset = filtersets.DeviceFilterSet @register_search() -class ModuleTypeIndex(SearchIndex): - model = ModuleType - queryset = ModuleType.objects.prefetch_related('manufacturer').annotate( - instance_count=count_related(Module, 'module_type') +class DeviceRoleIndex(SearchIndex): + model = models.DeviceRole + fields = ( + ('name', 100), + ('slug', 100), + ('description', 500), ) - filterset = dcim.filtersets.ModuleTypeFilterSet - table = dcim.tables.ModuleTypeTable - url = 'dcim:moduletype_list' + + +@register_search() +class DeviceTypeIndex(SearchIndex): + model = models.DeviceType + fields = ( + ('model', 100), + ('part_number', 200), + ('comments', 1000), + ) + queryset = models.DeviceType.objects.prefetch_related('manufacturer').annotate( + instance_count=count_related(models.Device, 'device_type') + ) + filterset = filtersets.DeviceTypeFilterSet + + +@register_search() +class LocationIndex(SearchIndex): + model = models.Location + fields = ( + ('name', 100), + ('slug', 100), + ('description', 500), + ) + queryset = models.Location.objects.add_related_count( + models.Location.objects.add_related_count( + models.Location.objects.all(), models.Device, 'location', 'device_count', cumulative=True + ), + models.Rack, + 'location', + 'rack_count', + cumulative=True, + ).prefetch_related('site') + filterset = filtersets.LocationFilterSet @register_search() class ModuleIndex(SearchIndex): - model = Module - queryset = Module.objects.prefetch_related( + model = models.Module + fields = ( + ('asset_tag', 50), + ('serial', 60), + ('comments', 1000), + ) + queryset = models.Module.objects.prefetch_related( 'module_type__manufacturer', 'device', 'module_bay', ) - filterset = dcim.filtersets.ModuleFilterSet - table = dcim.tables.ModuleTable - url = 'dcim:module_list' + filterset = filtersets.ModuleFilterSet @register_search() -class VirtualChassisIndex(SearchIndex): - model = VirtualChassis - queryset = VirtualChassis.objects.prefetch_related('master').annotate( - member_count=count_related(Device, 'virtual_chassis') +class ModuleTypeIndex(SearchIndex): + model = models.ModuleType + fields = ( + ('model', 100), + ('part_number', 200), + ('comments', 1000), ) - filterset = dcim.filtersets.VirtualChassisFilterSet - table = dcim.tables.VirtualChassisTable - url = 'dcim:virtualchassis_list' - - -@register_search() -class CableIndex(SearchIndex): - model = Cable - queryset = Cable.objects.all() - filterset = dcim.filtersets.CableFilterSet - table = dcim.tables.CableTable - url = 'dcim:cable_list' + queryset = models.ModuleType.objects.prefetch_related('manufacturer').annotate( + instance_count=count_related(models.Module, 'module_type') + ) + filterset = filtersets.ModuleTypeFilterSet @register_search() class PowerFeedIndex(SearchIndex): - model = PowerFeed - queryset = PowerFeed.objects.all() - filterset = dcim.filtersets.PowerFeedFilterSet - table = dcim.tables.PowerFeedTable - url = 'dcim:powerfeed_list' + model = models.PowerFeed + fields = ( + ('name', 100), + ('comments', 1000), + ) + queryset = models.PowerFeed.objects.all() + filterset = filtersets.PowerFeedFilterSet + + +@register_search() +class RackIndex(SearchIndex): + model = models.Rack + fields = ( + ('asset_tag', 50), + ('serial', 60), + ('name', 100), + ('facility_id', 100), + ('comments', 1000), + ) + queryset = models.Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate( + device_count=count_related(models.Device, 'rack') + ) + filterset = filtersets.RackFilterSet + + +@register_search() +class RackReservationIndex(SearchIndex): + model = models.RackReservation + fields = ( + ('description', 500), + ) + queryset = models.RackReservation.objects.prefetch_related('rack', 'user') + filterset = filtersets.RackReservationFilterSet + + +@register_search() +class RegionIndex(SearchIndex): + model = models.Region + fields = ( + ('name', 100), + ('slug', 100), + ('description', 500) + ) + + +@register_search() +class SiteIndex(SearchIndex): + model = models.Site + fields = ( + ('name', 100), + ('facility', 100), + ('description', 500), + ('physical_address', 1000), + ('shipping_address', 1000), + ) + queryset = models.Site.objects.prefetch_related('region', 'tenant', 'tenant__group') + filterset = filtersets.SiteFilterSet + + +@register_search() +class SiteGroupIndex(SearchIndex): + model = models.SiteGroup + fields = ( + ('name', 100), + ('slug', 100), + ('description', 500) + ) + + +@register_search() +class VirtualChassisIndex(SearchIndex): + model = models.VirtualChassis + fields = ( + ('name', 100), + ('domain', 300) + ) + queryset = models.VirtualChassis.objects.prefetch_related('master').annotate( + member_count=count_related(models.Device, 'virtual_chassis') + ) + filterset = filtersets.VirtualChassisFilterSet diff --git a/netbox/extras/management/commands/reindex.py b/netbox/extras/management/commands/reindex.py new file mode 100644 index 000000000..249a5e5f8 --- /dev/null +++ b/netbox/extras/management/commands/reindex.py @@ -0,0 +1,25 @@ +from django.core.management.base import BaseCommand + +from extras.models import CachedValue +from extras.registry import registry +from netbox.search.backends import search_backend + + +class Command(BaseCommand): + """Reindex cached search values""" + help = 'Reindex cached search values.' + + def handle(self, *args, **kwargs): + + self.stdout.write('Clearing cached values...', ending="\n") + CachedValue.objects.all().delete() + + for app_label, models in registry['search'].items(): + for name, idx in models.items(): + self.stdout.write(f'Reindexing {app_label}.{name}...', ending="\n") + model = idx.model + for instance in model.objects.all(): + search_backend.cache(model, instance) + + cache_size = CachedValue.objects.count() + self.stdout.write(f'Done. Generated {cache_size} cached values', ending="\n") diff --git a/netbox/extras/migrations/0079_search.py b/netbox/extras/migrations/0079_search.py new file mode 100644 index 000000000..2ca054866 --- /dev/null +++ b/netbox/extras/migrations/0079_search.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.1 on 2022-10-10 18:22 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('extras', '0078_unique_constraints'), + ] + + operations = [ + migrations.CreateModel( + name='CachedValue', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('object_id', models.PositiveBigIntegerField()), + ('field', models.CharField(max_length=200)), + ('type', models.CharField(max_length=30)), + ('value', models.TextField()), + ('weight', models.PositiveSmallIntegerField(default=1000)), + ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')), + ], + options={ + 'ordering': ('weight', 'pk'), + }, + ), + ] diff --git a/netbox/extras/models/__init__.py b/netbox/extras/models/__init__.py index 3cb6372be..e3a4be3fe 100644 --- a/netbox/extras/models/__init__.py +++ b/netbox/extras/models/__init__.py @@ -2,9 +2,11 @@ from .change_logging import ObjectChange from .configcontexts import ConfigContext, ConfigContextModel from .customfields import CustomField from .models import * +from .search import * from .tags import Tag, TaggedItem __all__ = ( + 'CachedValue', 'ConfigContext', 'ConfigContextModel', 'ConfigRevision', diff --git a/netbox/extras/models/search.py b/netbox/extras/models/search.py new file mode 100644 index 000000000..b163b4edc --- /dev/null +++ b/netbox/extras/models/search.py @@ -0,0 +1,40 @@ +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.db import models + +__all__ = ( + 'CachedValue', +) + + +class CachedValue(models.Model): + timestamp = models.DateTimeField( + auto_now_add=True, + editable=False + ) + object_type = models.ForeignKey( + to=ContentType, + on_delete=models.CASCADE, + related_name='+' + ) + object_id = models.PositiveBigIntegerField() + object = GenericForeignKey( + ct_field='object_type', + fk_field='object_id' + ) + field = models.CharField( + max_length=200 + ) + type = models.CharField( + max_length=30 + ) + value = models.TextField() + weight = models.PositiveSmallIntegerField( + default=1000 + ) + + class Meta: + ordering = ('weight', 'pk') + + def __str__(self): + return f'{self.object_type} {self.object_id}: {self.field}={self.value}' diff --git a/netbox/extras/search.py b/netbox/extras/search.py index ae6c9e7b9..ab2e3787d 100644 --- a/netbox/extras/search.py +++ b/netbox/extras/search.py @@ -7,8 +7,9 @@ from netbox.search import SearchIndex, register_search @register_search() class JournalEntryIndex(SearchIndex): model = JournalEntry + fields = ( + ('comments', 1000), + ) queryset = JournalEntry.objects.prefetch_related('assigned_object', 'created_by') filterset = extras.filtersets.JournalEntryFilterSet - table = extras.tables.JournalEntryTable - url = 'extras:journalentry_list' category = 'Journal' diff --git a/netbox/extras/tests/dummy_plugin/search.py b/netbox/extras/tests/dummy_plugin/search.py index 4a1b7e666..a596ec7b8 100644 --- a/netbox/extras/tests/dummy_plugin/search.py +++ b/netbox/extras/tests/dummy_plugin/search.py @@ -5,7 +5,6 @@ from .models import DummyModel class DummyModelIndex(SearchIndex): model = DummyModel queryset = DummyModel.objects.all() - url = 'plugins:dummy_plugin:dummy_models' indexes = ( diff --git a/netbox/ipam/search.py b/netbox/ipam/search.py index 2f4599321..254d85869 100644 --- a/netbox/ipam/search.py +++ b/netbox/ipam/search.py @@ -4,66 +4,84 @@ from ipam.models import ASN, VLAN, VRF, Aggregate, IPAddress, Prefix, Service from netbox.search import SearchIndex, register_search -@register_search() -class VRFIndex(SearchIndex): - model = VRF - queryset = VRF.objects.prefetch_related('tenant', 'tenant__group') - filterset = ipam.filtersets.VRFFilterSet - table = ipam.tables.VRFTable - url = 'ipam:vrf_list' - - @register_search() class AggregateIndex(SearchIndex): model = Aggregate + fields = ( + ('prefix', 100), + ('description', 500), + ('date_added', 2000), + ) queryset = Aggregate.objects.prefetch_related('rir') filterset = ipam.filtersets.AggregateFilterSet - table = ipam.tables.AggregateTable - url = 'ipam:aggregate_list' - - -@register_search() -class PrefixIndex(SearchIndex): - model = Prefix - queryset = Prefix.objects.prefetch_related( - 'site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role' - ) - filterset = ipam.filtersets.PrefixFilterSet - table = ipam.tables.PrefixTable - url = 'ipam:prefix_list' - - -@register_search() -class IPAddressIndex(SearchIndex): - model = IPAddress - queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group') - filterset = ipam.filtersets.IPAddressFilterSet - table = ipam.tables.IPAddressTable - url = 'ipam:ipaddress_list' - - -@register_search() -class VLANIndex(SearchIndex): - model = VLAN - queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role') - filterset = ipam.filtersets.VLANFilterSet - table = ipam.tables.VLANTable - url = 'ipam:vlan_list' @register_search() class ASNIndex(SearchIndex): model = ASN + fields = ( + ('asn', 100), + ('description', 500), + ) queryset = ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group') filterset = ipam.filtersets.ASNFilterSet - table = ipam.tables.ASNTable - url = 'ipam:asn_list' + + +@register_search() +class IPAddressIndex(SearchIndex): + model = IPAddress + fields = ( + ('address', 100), + ('dns_name', 300), + ('description', 500), + ) + queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group') + filterset = ipam.filtersets.IPAddressFilterSet + + +@register_search() +class PrefixIndex(SearchIndex): + model = Prefix + fields = ( + ('prefix', 100), + ('description', 500), + ) + queryset = Prefix.objects.prefetch_related( + 'site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role' + ) + filterset = ipam.filtersets.PrefixFilterSet @register_search() class ServiceIndex(SearchIndex): model = Service + fields = ( + ('name', 100), + ('description', 500), + ) queryset = Service.objects.prefetch_related('device', 'virtual_machine') filterset = ipam.filtersets.ServiceFilterSet - table = ipam.tables.ServiceTable - url = 'ipam:service_list' + + +@register_search() +class VLANIndex(SearchIndex): + model = VLAN + fields = ( + ('name', 100), + ('vid', 100), + ('description', 500), + ) + queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role') + filterset = ipam.filtersets.VLANFilterSet + + +@register_search() +class VRFIndex(SearchIndex): + model = VRF + fields = ( + ('name', 100), + ('rd', 200), + ('description', 500), + ) + queryset = VRF.objects.prefetch_related('tenant', 'tenant__group') + filterset = ipam.filtersets.VRFFilterSet diff --git a/netbox/netbox/forms/__init__.py b/netbox/netbox/forms/__init__.py index eb1311d98..b2ee6f706 100644 --- a/netbox/netbox/forms/__init__.py +++ b/netbox/netbox/forms/__init__.py @@ -1,6 +1,6 @@ from django import forms -from netbox.search.backends import default_search_engine +from netbox.search.backends import search_backend from utilities.forms import BootstrapMixin from .base import * @@ -26,13 +26,13 @@ class SearchForm(BootstrapMixin, forms.Form): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["obj_type"] = forms.ChoiceField( - choices=default_search_engine.get_search_choices(), + choices=search_backend.get_search_choices(), required=False, label='Type' ) def get_options(self): if not self.options: - self.options = build_options(default_search_engine.get_search_choices()) + self.options = build_options(search_backend.get_search_choices()) return self.options diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index f59e72c14..8494ee011 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -9,6 +9,7 @@ from django.db import models from taggit.managers import TaggableManager from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices +from extras.registry import registry from extras.utils import is_taggable, register_features from netbox.signals import post_clean from utilities.json import CustomFieldJSONEncoder diff --git a/netbox/netbox/search/__init__.py b/netbox/netbox/search/__init__.py index 0664dc6ca..8256f95c6 100644 --- a/netbox/netbox/search/__init__.py +++ b/netbox/netbox/search/__init__.py @@ -9,6 +9,7 @@ class SearchIndex: model: The model class for which this index is used. """ model = None + fields = () @classmethod def get_category(cls): @@ -19,6 +20,13 @@ class SearchIndex: return cls.category return cls.model._meta.app_config.verbose_name + @classmethod + def to_cache(cls, instance): + return [ + (field, str(getattr(instance, field)), weight) + for field, weight in cls.fields + ] + def register_search(): def _wrapper(cls): diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index b6cead5bd..e90415727 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -2,9 +2,11 @@ from collections import defaultdict from importlib import import_module from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured -from django.urls import reverse +from django.db.models.signals import post_save +from extras.models import CachedValue from extras.registry import registry from netbox.constants import SEARCH_MAX_RESULTS @@ -12,6 +14,13 @@ from netbox.constants import SEARCH_MAX_RESULTS _backends_cache = {} +def get_indexer(model): + app_label = model._meta.app_label + model_name = model._meta.model_name + + return registry['search'][app_label][model_name] + + class SearchEngineError(Exception): """Something went wrong with a search engine.""" pass @@ -21,6 +30,11 @@ class SearchBackend: """A search engine capable of performing multi-table searches.""" _search_choice_options = tuple() + def __init__(self): + + # Connect cache handler to the model post-save signal + post_save.connect(self.cache) + def get_registry(self): r = {} for app_label, models in registry['search'].items(): @@ -53,7 +67,8 @@ class SearchBackend: """Execute a search query for the given value.""" raise NotImplementedError - def cache(self, instance): + @staticmethod + def cache(sender, instance, **kwargs): """Create or update the cached copy of an instance.""" raise NotImplementedError @@ -70,7 +85,6 @@ class FilterSetSearchBackend(SearchBackend): for obj_type in search_registry.keys(): queryset = search_registry[obj_type].queryset - url = search_registry[obj_type].url # Restrict the queryset for the current user if hasattr(queryset, 'restrict'): @@ -81,30 +95,51 @@ class FilterSetSearchBackend(SearchBackend): # This backend requires a FilterSet class for the model continue - table = getattr(search_registry[obj_type], 'table', None) - if not table: - # This backend requires a Table class for the model - continue + queryset = filterset({'q': value}, queryset=queryset).qs[:SEARCH_MAX_RESULTS] - # Construct the results table for this object type - filtered_queryset = filterset({'q': value}, queryset=queryset).qs - table = table(filtered_queryset, orderable=False) - table.paginate(per_page=SEARCH_MAX_RESULTS) - - if table.page: - results.append({ - 'name': queryset.model._meta.verbose_name_plural, - 'table': table, - 'url': f"{reverse(url)}?q={value}" - }) + results.extend([ + {'object': obj} + for obj in queryset + ]) return results - def cache(self, instance): + @staticmethod + def cache(sender, instance, **kwargs): # This backend does not utilize a cache pass +class CachedValueSearchBackend(SearchBackend): + + def search(self, request, value, **kwargs): + return CachedValue.objects.filter(value__icontains=value) + + @staticmethod + def cache(sender, instance, **kwargs): + try: + indexer = get_indexer(instance) + except KeyError: + return + + data = indexer.to_cache(instance) + + for field, value, weight in data: + if not value: + continue + ct = ContentType.objects.get_for_model(instance) + CachedValue.objects.update_or_create( + defaults={ + 'value': value, + 'weight': weight, + }, + object_type=ct, + object_id=instance.pk, + field=field, + type='text' # TODO + ) + + def get_backend(): """Initializes and returns the configured search backend.""" backend_name = settings.SEARCH_BACKEND @@ -121,5 +156,4 @@ def get_backend(): return backend_cls() -default_search_engine = get_backend() -search = default_search_engine.search +search_backend = get_backend() diff --git a/netbox/netbox/views/__init__.py b/netbox/netbox/views/__init__.py index d880ba64c..ebc3c28bb 100644 --- a/netbox/netbox/views/__init__.py +++ b/netbox/netbox/views/__init__.py @@ -23,7 +23,7 @@ from extras.tables import ObjectChangeTable from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF from netbox.constants import SEARCH_MAX_RESULTS from netbox.forms import SearchForm -from netbox.search.backends import default_search_engine +from netbox.search.backends import search_backend from tenancy.models import Tenant from virtualization.models import Cluster, VirtualMachine from wireless.models import WirelessLAN, WirelessLink @@ -153,14 +153,14 @@ class SearchView(View): results = [] if form.is_valid(): - search_registry = default_search_engine.get_registry() + search_registry = search_backend.get_registry() # If an object type has been specified, redirect to the dedicated view for it if form.cleaned_data['obj_type']: object_type = form.cleaned_data['obj_type'] url = reverse(search_registry[object_type].url) return redirect(f"{url}?q={form.cleaned_data['q']}") - results = default_search_engine.search(request, form.cleaned_data['q']) + results = search_backend.search(request, form.cleaned_data['q']) return render(request, 'search.html', { 'form': form, diff --git a/netbox/templates/search.html b/netbox/templates/search.html index a47b48b09..e3eeb69d5 100644 --- a/netbox/templates/search.html +++ b/netbox/templates/search.html @@ -20,44 +20,29 @@ {% if request.GET.q %} {% if results %}
-
- {% for obj_type in results %} -
-
{{ obj_type.name|bettertitle }}
-
- {% render_table obj_type.table 'inc/table.html' %} -
- -
- {% endfor %} -
-
-
-
- Search Results -
- +
+
+
+ + + + + + + + {% for result in results %} + + + + + + + {% endfor %} +
TypeObjectFieldValue
{{ result.object|content_type }} + {{ result.object }} + {{ result.field|placeholder }}{{ result.value|placeholder }}
+
{% else %} diff --git a/netbox/tenancy/search.py b/netbox/tenancy/search.py index e52b1859e..e00f6d933 100644 --- a/netbox/tenancy/search.py +++ b/netbox/tenancy/search.py @@ -5,21 +5,32 @@ from tenancy.models import Contact, ContactAssignment, Tenant from utilities.utils import count_related -@register_search() -class TenantIndex(SearchIndex): - model = Tenant - queryset = Tenant.objects.prefetch_related('group') - filterset = tenancy.filtersets.TenantFilterSet - table = tenancy.tables.TenantTable - url = 'tenancy:tenant_list' - - @register_search() class ContactIndex(SearchIndex): model = Contact + fields = ( + ('name', 100), + ('title', 200), + ('phone', 200), + ('email', 200), + ('address', 200), + ('link', 300), + ('comments', 1000), + ) queryset = Contact.objects.prefetch_related('group', 'assignments').annotate( assignment_count=count_related(ContactAssignment, 'contact') ) filterset = tenancy.filtersets.ContactFilterSet - table = tenancy.tables.ContactTable - url = 'tenancy:contact_list' + + +@register_search() +class TenantIndex(SearchIndex): + model = Tenant + fields = ( + ('name', 100), + ('slug', 100), + ('description', 500), + ('comments', 1000), + ) + queryset = Tenant.objects.prefetch_related('group') + filterset = tenancy.filtersets.TenantFilterSet diff --git a/netbox/virtualization/search.py b/netbox/virtualization/search.py index 5b24f7fa0..25c3ee647 100644 --- a/netbox/virtualization/search.py +++ b/netbox/virtualization/search.py @@ -9,17 +9,23 @@ from virtualization.models import Cluster, VirtualMachine @register_search() class ClusterIndex(SearchIndex): model = Cluster + fields = ( + ('name', 100), + ('comments', 1000), + ) queryset = Cluster.objects.prefetch_related('type', 'group').annotate( device_count=count_related(Device, 'cluster'), vm_count=count_related(VirtualMachine, 'cluster') ) filterset = virtualization.filtersets.ClusterFilterSet - table = virtualization.tables.ClusterTable - url = 'virtualization:cluster_list' @register_search() class VirtualMachineIndex(SearchIndex): model = VirtualMachine + fields = ( + ('name', 100), + ('comments', 1000), + ) queryset = VirtualMachine.objects.prefetch_related( 'cluster', 'tenant', @@ -29,5 +35,3 @@ class VirtualMachineIndex(SearchIndex): 'primary_ip6', ) filterset = virtualization.filtersets.VirtualMachineFilterSet - table = virtualization.tables.VirtualMachineTable - url = 'virtualization:virtualmachine_list' diff --git a/netbox/wireless/search.py b/netbox/wireless/search.py index 89ac23af8..151aff694 100644 --- a/netbox/wireless/search.py +++ b/netbox/wireless/search.py @@ -9,18 +9,24 @@ from wireless.models import WirelessLAN, WirelessLink @register_search() class WirelessLANIndex(SearchIndex): model = WirelessLAN + fields = ( + ('ssid', 100), + ('description', 500), + ('auth_psk', 1000), + ) queryset = WirelessLAN.objects.prefetch_related('group', 'vlan').annotate( interface_count=count_related(Interface, 'wireless_lans') ) filterset = wireless.filtersets.WirelessLANFilterSet - table = wireless.tables.WirelessLANTable - url = 'wireless:wirelesslan_list' @register_search() class WirelessLinkIndex(SearchIndex): model = WirelessLink + fields = ( + ('ssid', 100), + ('description', 500), + ('auth_psk', 1000), + ) queryset = WirelessLink.objects.prefetch_related('interface_a__device', 'interface_b__device') filterset = wireless.filtersets.WirelessLinkFilterSet - table = wireless.tables.WirelessLinkTable - url = 'wireless:wirelesslink_list'