1
0
mirror of https://github.com/peeringdb/peeringdb.git synced 2024-05-11 05:55:09 +00:00
Files
peeringdb-peeringdb/peeringdb_server/search.py

218 lines
5.3 KiB
Python
Raw Normal View History

"""
Search implementation used for the peeringdb top search bar, name
searches through the api `name_search` filter, as well as advanced
search functionality.
Search logic is handled by django-haystack and whoosh.
Refer to search_indexes.py for search index definition.
"""
# import time
import re
Support 202111 (#1101) * Block registering private ASN ranges * Add a continental region field for facilities #1007 * Incorrect order of search results #232 * Allow users to upload a small logo to their record #346 * Sponsor badge CSS and text translation issues #453 * IXP and Facility summary #18 * Add sales email and phone contact to ix object #949 * Increase timeout timer for IX-F JSON importer to 30s #962 * IX-F manually triggered import bugs #1054 * remove survey notifications * IX-F importer: manual add followed by IX-F prompted add can result in "The server rejected your data" #807 * performance fix for net view * api documentation generate broken #956 * fix poc set missing from net view * Auto focus cursor on search field on main website #695 * PeeringDB website has a poor choice of line-breaks for IPv6 addresses. #748 * Delete childless org objects #838 * linting * poetry lock * re-add mistakingly dropped translation to email template * fix template variables * regen docs * regen api docs * add u2f api * fix merge cruft * fido u2f webauthn first steps * dont need this * typo * first ux steps for security key management, u2f views * u2f auth * linting * Cascade delete when performed by superuser in django-admin #735 * security key passwordless login * reset webauthn migrations * security key 2fa * linting * ux cleanup * cleanup * Add IXP to AS record / dropdown limited #469 * ignore orgs that have pre-dated or finished sponsorships attached * split out to django_security_keys * default to 90 days * make URL required for new objects #374 * move CONN_MAX_AGE to proper location and expose as env var (#1060) * Error message for invalid password on email change * Registering a new facility or exchange organization is broken #1062 * Creating a facility that matches the name of a soft-deleted facility will cause the entry to bypass the verification queue #901 * irr source validator doesn't allow for hyphens in source #921 * split out javascript * split out js * Clicking "Add" to add a user api-key without providing a name for the key raises Internal Error #1033 * remove security key admin views * global stats don't show up at login screen #284 * Possible for "pending" exchange to have "deleted" ixlan (#1077) * webauthn finalizations * oidc support for oauth provider #1070 * Revert "Merge branch 'gh_1070' into 'support_202111'" * remove unused dev mounts * linting * add django-security-key and relock poetry * re-add imports removed through linting * docs regen * oauth toolkit pinned to 1.6.1 django-security-keys pinned to 1.0.1 * remove debug message * linting * linting Co-authored-by: Stefan Pratter <stefan@20c.com> Co-authored-by: David Poarch <dpoarch@20c.com>
2022-01-11 08:56:47 -06:00
import unidecode
from django.conf import settings
from django.db.models import Q
from haystack.inputs import Exact
from haystack.query import SearchQuerySet
from peeringdb_server.models import (
Facility,
InternetExchange,
IXLanPrefix,
Network,
NetworkIXLan,
Organization,
)
# models considered during autocomplete (quick-search)
autocomplete_models = [
Organization,
Network,
InternetExchange,
Facility,
]
# models considered during standard search
searchable_models = [
Organization,
Network,
Facility,
InternetExchange,
NetworkIXLan,
IXLanPrefix,
# InternetExchangeFacility,
# NetworkFacility,
# NetworkContact,
# IXLan,
]
2019-12-05 16:57:52 +00:00
ONLY_DIGITS = re.compile(r"^[0-9]+$")
# These are not exact, but should be good enough
PARTIAL_IPV4_ADDRESS = re.compile(r"^([0-9]{1,3}\.){1,3}([0-9]{1,3})?$")
PARTIAL_IPV6_ADDRESS = re.compile(r"^([0-9A-Fa-f]{1,4}|:):[0-9A-Fa-f:]*$")
def unaccent(v):
return unidecode.unidecode(v).lower().strip()
def valid_partial_ipv4_address(ip):
return all(int(s) >= 0 and int(s) <= 255 for s in ip.split(".") if len(s) > 0)
def make_asn_query(term):
return Network.objects.filter(asn__exact=term, status="ok")
def make_ipv4_query(term):
return NetworkIXLan.objects.filter(ipaddr4__startswith=term, status="ok")
def make_ipv6_query(term):
return NetworkIXLan.objects.filter(ipaddr6__startswith=term, status="ok")
2018-11-08 19:45:21 +00:00
2019-12-05 16:57:52 +00:00
def prepare_term(term):
try:
if len(term) == 1:
int(term)
term = f"AS{term}"
except ValueError:
pass
2018-11-08 19:45:21 +00:00
return unaccent(term)
2018-11-08 19:45:21 +00:00
def make_search_query(term):
if not term:
return SearchQuerySet().none()
2018-11-08 19:45:21 +00:00
term = unaccent(term)
if ONLY_DIGITS.match(term):
return make_asn_query(term)
if PARTIAL_IPV4_ADDRESS.match(term):
if valid_partial_ipv4_address(term):
return make_ipv4_query(term)
if PARTIAL_IPV6_ADDRESS.match(term):
return make_ipv6_query(term)
2018-11-08 19:45:21 +00:00
term_filters = Q(content=term) | Q(content__startswith=term)
2018-11-08 19:45:21 +00:00
return (
SearchQuerySet()
.filter(term_filters, status=Exact("ok"))
.models(*searchable_models)
)
2018-11-08 19:45:21 +00:00
def make_name_search_query(term):
if not term:
return SearchQuerySet().none()
term = prepare_term(term)
2018-11-08 19:45:21 +00:00
term_filters = Q(name=term) | Q(name__startswith=term)
2018-11-08 19:45:21 +00:00
return SearchQuerySet().filter(term_filters, status=Exact("ok"))
2018-11-08 19:45:21 +00:00
def make_autocomplete_query(term):
if not term:
return SearchQuerySet().none()
2018-11-08 19:45:21 +00:00
term = prepare_term(term)
return SearchQuerySet().autocomplete(auto=term).filter(status=Exact("ok"))
2018-11-08 19:45:21 +00:00
def search(term, autocomplete=False):
2018-11-08 19:45:21 +00:00
"""
Search searchable objects (ixp, network, facility ...) by term.
2018-11-08 19:45:21 +00:00
Returns result dict.
2018-11-08 19:45:21 +00:00
"""
# t0 = time.time()
2018-11-08 19:45:21 +00:00
if autocomplete:
search_query = make_autocomplete_query(term).models(*autocomplete_models)
limit = settings.SEARCH_RESULTS_AUTOCOMPLETE_LIMIT
2018-11-08 19:45:21 +00:00
else:
search_query = make_search_query(term)
limit = settings.SEARCH_RESULTS_LIMIT
2018-11-08 19:45:21 +00:00
categories = ("fac", "ix", "net", "org")
result = {tag: [] for tag in categories}
pk_map = {tag: {} for tag in categories}
# add entries to the result by order of scoring with the
# highest scored on top (beginning of list)
for sq in search_query[:limit]:
if hasattr(sq, "model"):
model = sq.model
model.HandleRef.tag
categorize(sq, result, pk_map)
else:
if sq.HandleRef.tag == "netixlan":
add_secondary_entries(sq, result, pk_map)
else:
append_result(
sq.HandleRef.tag,
sq.pk,
getattr(sq, "search_result_name", None),
sq.org_id,
None,
result,
pk_map,
)
# print("done", time.time() - t0)
return result
def categorize(sq, result, pk_map):
if getattr(sq, "result_name", None):
# main entity
tag = sq.model.HandleRef.tag
if tag == "org":
org_id = int(sq.pk)
else:
org_id = sq.org_id
append_result(tag, int(sq.pk), sq.result_name, org_id, None, result, pk_map)
else:
add_secondary_entries(sq, result, pk_map)
2018-11-08 19:45:21 +00:00
def add_secondary_entries(sq, result, pk_map):
for tag in result.keys():
if not getattr(sq, f"{tag}_result_name", None):
continue
org_id = int(getattr(sq, f"{tag}_org_id", 0))
name = getattr(sq, f"{tag}_result_name")
pk = int(getattr(sq, f"{tag}_id", 0))
sub_name = getattr(sq, f"{tag}_sub_result_name")
append_result(tag, pk, name, org_id, sub_name, result, pk_map)
def append_result(tag, pk, name, org_id, sub_name, result, pk_map):
if pk in pk_map[tag]:
return
pk_map[tag][pk] = True
result[tag].append(
{"id": pk, "name": name, "org_id": int(org_id), "sub_name": sub_name}
)