diff --git a/ietf/doc/tasks.py b/ietf/doc/tasks.py index 273242e35f..37c235b911 100644 --- a/ietf/doc/tasks.py +++ b/ietf/doc/tasks.py @@ -212,10 +212,14 @@ def update_rfc_searchindex_task(self, rfc_number: int): @shared_task -def rebuild_searchindex_task(*, batchsize=40, drop_collection=False): +def rebuild_searchindex_task( + *, batchsize=40, drop_collection=False, upsert_presets=True +): if drop_collection: searchindex.delete_collection() searchindex.create_collection() + if upsert_presets: + searchindex.upsert_presets() # ok if they already exist searchindex.update_or_create_rfc_entries( Document.objects.filter(type_id="rfc").order_by("-rfc_number"), batchsize=batchsize, diff --git a/ietf/doc/tests_tasks.py b/ietf/doc/tests_tasks.py index 2e2d65463f..48db95d047 100644 --- a/ietf/doc/tests_tasks.py +++ b/ietf/doc/tests_tasks.py @@ -146,13 +146,17 @@ def test_update_rfc_searchindex_task( update_rfc_searchindex_task(rfc_number=rfc.rfc_number) @mock.patch("ietf.doc.tasks.searchindex.update_or_create_rfc_entries") + @mock.patch("ietf.doc.tasks.searchindex.upsert_presets") @mock.patch("ietf.doc.tasks.searchindex.create_collection") @mock.patch("ietf.doc.tasks.searchindex.delete_collection") - def test_rebuild_searchindex_task(self, mock_delete, mock_create, mock_update): + def test_rebuild_searchindex_task( + self, mock_delete, mock_create, mock_presets, mock_update + ): rfcs = WgRfcFactory.create_batch(10) rebuild_searchindex_task() self.assertFalse(mock_delete.called) self.assertFalse(mock_create.called) + self.assertTrue(mock_presets.called) self.assertTrue(mock_update.called) self.assertQuerysetEqual( mock_update.call_args.args[0], @@ -162,10 +166,12 @@ def test_rebuild_searchindex_task(self, mock_delete, mock_create, mock_update): mock_delete.reset_mock() mock_create.reset_mock() + mock_presets.reset_mock() mock_update.reset_mock() rebuild_searchindex_task(drop_collection=True) self.assertTrue(mock_delete.called) self.assertTrue(mock_create.called) + self.assertTrue(mock_presets.called) self.assertTrue(mock_update.called) self.assertQuerysetEqual( mock_update.call_args.args[0], @@ -175,10 +181,14 @@ def test_rebuild_searchindex_task(self, mock_delete, mock_create, mock_update): mock_delete.reset_mock() mock_create.reset_mock() + mock_presets.reset_mock() mock_update.reset_mock() - rebuild_searchindex_task(drop_collection=True, batchsize=3) + rebuild_searchindex_task( + drop_collection=True, batchsize=3, upsert_presets=False + ) self.assertTrue(mock_delete.called) self.assertTrue(mock_create.called) + self.assertFalse(mock_presets.called) self.assertTrue(mock_update.called) self.assertQuerysetEqual( mock_update.call_args.args[0], diff --git a/ietf/utils/searchindex.py b/ietf/utils/searchindex.py index 87951abb60..ffc139676e 100644 --- a/ietf/utils/searchindex.py +++ b/ietf/utils/searchindex.py @@ -5,8 +5,10 @@ from itertools import batched from math import floor from typing import Iterable +from urllib.parse import urljoin import httpx # just for exceptions +import requests import typesense import typesense.exceptions from django.conf import settings @@ -26,7 +28,6 @@ typesense.exceptions.ServiceUnavailable, ) - DEFAULT_SETTINGS = { "TYPESENSE_API_URL": "", "TYPESENSE_API_KEY": "", @@ -144,7 +145,7 @@ def typesense_doc_from_rfc(rfc: Document) -> DocumentSchema: if subseries is not None: ts_document["subseries"] = { "acronym": subseries.type.slug, - "number": int(subseries.name[len(subseries.type.slug) :]), + "number": int(subseries.name[len(subseries.type.slug):]), "total": len(subseries.contains()), } if rfc.group is not None: @@ -354,6 +355,21 @@ def update_or_create_rfc_entries( ], } +SEARCH_PRESETS = { + "red": { + "collection": "docs", + "infix": "off,always,off,off,off,off,off,off", + "query_by": "rfc,filename,title,abstract,keywords,authors,group,area", + "query_by_weights": "127,50,50,20,20,5,2,1" + }, + "red-content": { + "collection": "docs", + "infix": "off,always,off,off", + "query_by": "rfc,filename,authors,content", + "query_by_weights": "127,50,5,1" + }, +} + def create_collection(): collection_name = get_collection_name() @@ -370,3 +386,21 @@ def delete_collection(): client.collections[collection_name].delete() except typesense.exceptions.ObjectNotFound: pass + + +def upsert_presets(): + # typesense-python does not support presets, so use requests + _settings = get_settings() + api_base = _settings["TYPESENSE_API_URL"] + api_key = _settings["TYPESENSE_API_KEY"] + for preset_name, payload in SEARCH_PRESETS.items(): + log(f"Upserting '{preset_name}' preset") + response = requests.put( + urljoin(api_base, f"/presets/{preset_name}"), + json={"value": payload}, + headers={ + "X-TYPESENSE-API-KEY": api_key, + }, + timeout=3, + ) + response.raise_for_status() diff --git a/ietf/utils/tests_searchindex.py b/ietf/utils/tests_searchindex.py index e9fbf52020..ffca5c18be 100644 --- a/ietf/utils/tests_searchindex.py +++ b/ietf/utils/tests_searchindex.py @@ -1,6 +1,7 @@ # Copyright The IETF Trust 2026, All Rights Reserved from unittest import mock +import requests.exceptions import typesense.exceptions from django.conf import settings from django.test.utils import override_settings @@ -211,3 +212,34 @@ def test_delete_collection(self, mock_ts_client_constructor): mock_collections["frogs"].side_effect = typesense.exceptions.ObjectNotFound searchindex.delete_collection() # should ignore the exception + + @override_settings( + SEARCHINDEX_CONFIG={ + "TYPESENSE_API_URL": "http://ts.example.com", + "TYPESENSE_API_KEY": "test-api-key", + "TYPESENSE_COLLECTION_NAME": "frogs", + } + ) + def test_upsert_presets(self): + self.requests_mock.put( + "http://ts.example.com/presets/red", text="ok", status_code=201 + ) + self.requests_mock.put( + "http://ts.example.com/presets/red-content", text="ok", status_code=202 + ) + searchindex.upsert_presets() + + self.requests_mock.put( + "http://ts.example.com/presets/red", text="not ok", status_code=400 + ) + with self.assertRaises(requests.exceptions.HTTPError): + searchindex.upsert_presets() + + self.requests_mock.put( + "http://ts.example.com/presets/red", text="ok", status_code=200 + ) + self.requests_mock.put( + "http://ts.example.com/presets/red-content", text="not ok", status_code=400 + ) + with self.assertRaises(requests.exceptions.HTTPError): + searchindex.upsert_presets()