search.py 2.93 KB
import math
import threading
import time

import typesense
from typesense import exceptions as typesense_exceptions

TYPESENSE_API = open('./docs/typesense').read().strip()
TYPESENSE_API, TYPESENSE_HOST = TYPESENSE_API.split(':')
client = typesense.Client({
	'api_key': TYPESENSE_API,
	'nodes': [{
		'host': TYPESENSE_HOST,
		'port': '8108',
		'protocol': 'http'
	}],
	'connection_timeout_seconds': 1.
})

COLLECTION = 'presup_actpro_fts'
schema = {
	'name': COLLECTION,
	'fields': [
        {'name': 'devengado', 'type': 'float'},
        {'name': 'is_class', 'type': 'bool'},
        {'name': 'class_', 'type': 'string'},
        {'name': 'texto', 'type': 'string'},
        {'name': 'metadatos', 'type': 'string'},
	],
	'default_sorting_field': 'devengado',
}

search_parameters = {
	"q": "cuba",
	"query_by": "texto",
	# "facet_by": "gestion",
	"sort_by": "devengado:desc",
	"per_page": 20,
}


AVAILABLE_CLASSES = [
	'programa_proyecto',
	'entidad',
	'objeto',
	'finfun',
	'acteco',
	'ubigeo',
	'rubro',
	'organismo',
]

BACKEND_ERRORS = (
	typesense_exceptions.Timeout,
	typesense_exceptions.ServiceUnavailable,
	typesense_exceptions.ServerError,
	typesense_exceptions.HTTPStatus0Error,
	OSError,
)
FAILURE_THRESHOLD = 2
OPEN_INTERVAL_SECONDS = 15.0


class SearchUnavailable(Exception):
	def __init__(self, retry_after: int):
		self.retry_after = retry_after
		super().__init__('search backend unavailable')


class SearchGuard:
	def __init__(self):
		self._lock = threading.Lock()
		self._failures = 0
		self._opened_until = 0.0

	def enter(self):
		now = time.monotonic()

		with self._lock:
			if self._opened_until > now:
				raise SearchUnavailable(max(1, math.ceil(self._opened_until - now)))

	def record_success(self):
		with self._lock:
			self._failures = 0
			self._opened_until = 0.0

	def record_failure(self):
		now = time.monotonic()

		with self._lock:
			self._failures += 1

			if self._failures >= FAILURE_THRESHOLD:
				self._opened_until = now + OPEN_INTERVAL_SECONDS
				return int(OPEN_INTERVAL_SECONDS)

		return 1


search_guard = SearchGuard()


def do_search(query, page, is_class, class_=None, order=None):
	search_guard.enter()

	search_parameters_ = search_parameters.copy()
	search_parameters_['q'] = query

	if is_class is not None:
		filter_c = ''
		if is_class:
			filter_c = 'is_class:true'
		else:
			filter_c = 'is_class:false'

		search_parameters_['filter_by'] = filter_c

	if (class_ is not None):
		class_ = [_ for _ in class_.split(',') if _ in AVAILABLE_CLASSES]
		if len(class_):
			search_parameters_['filter_by'] = 'class_:[{}]'.format(', '.join(class_))

	if (page is not None) and (page > 0) and (page < 100):
		search_parameters_['page'] = page

	if order == 'asc':
		search_parameters_['sort_by'] = 'devengado:asc'

	try:
		response = client.collections[COLLECTION].documents.search(
			search_parameters_
		)
	except BACKEND_ERRORS:
		raise SearchUnavailable(search_guard.record_failure())

	search_guard.record_success()
	return response