Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion ietf/doc/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 12 additions & 2 deletions ietf/doc/tests_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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],
Expand All @@ -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],
Expand Down
38 changes: 36 additions & 2 deletions ietf/utils/searchindex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,7 +28,6 @@
typesense.exceptions.ServiceUnavailable,
)


DEFAULT_SETTINGS = {
"TYPESENSE_API_URL": "",
"TYPESENSE_API_KEY": "",
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand All @@ -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()
32 changes: 32 additions & 0 deletions ietf/utils/tests_searchindex.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Loading