From 553b4625c2f9a8f925cf4620f6045066b14ec31a Mon Sep 17 00:00:00 2001 From: sudip-khanal Date: Thu, 25 Jun 2026 16:40:24 +0545 Subject: [PATCH 1/2] feat(report): add semantic search on report filter --- apps/reports/graphql/filters.py | 23 ++++++++++-- apps/reports/graphql/queries.py | 3 +- apps/reports/tests/queries_test.py | 2 +- schema.graphql | 57 ++++++++++++++++++++++++++---- 4 files changed, 73 insertions(+), 12 deletions(-) diff --git a/apps/reports/graphql/filters.py b/apps/reports/graphql/filters.py index 09a9f60..6c227d0 100644 --- a/apps/reports/graphql/filters.py +++ b/apps/reports/graphql/filters.py @@ -1,7 +1,11 @@ +from concurrent.futures import ThreadPoolExecutor +from uuid import UUID + import strawberry import strawberry_django from django.db.models import Q +from apps.reports.ai_features.search_docs import SearchReports from apps.reports.models import ( DocumentExtraction, DocumentExtractionStatus, @@ -15,6 +19,11 @@ from apps.reports.models import ReportType as ReportTypeEnum +def _get_ranked_ids(value: str) -> list[UUID]: + ranked_reports = SearchReports(query=value).rank_reports() + return [r.id for r in ranked_reports] + + @strawberry_django.filters.filter(Link, lookups=True) class LinkFilter: id: strawberry.ID | None = strawberry.UNSET @@ -43,10 +52,20 @@ class ReportFilter: thematic_area_id: strawberry.ID | None = strawberry.UNSET disaster_type: str | None = strawberry.UNSET report_type: ReportTypeEnum | None = strawberry.UNSET + title: strawberry.auto @strawberry_django.filter_field - def search(self, value: str, prefix: str) -> Q: - return Q(title__icontains=value) + def search(self, queryset, value: str, prefix: str): + if not value or not value.strip(): + return Q() + + with ThreadPoolExecutor(max_workers=1) as executor: + ids = executor.submit(_get_ranked_ids, value).result() + + if not ids: + return Q(pk__in=[]) # if nothing matches + + return Q(id__in=ids) @strawberry_django.filter_field def regions(self, queryset, value: list[strawberry.ID], prefix: str) -> Q: diff --git a/apps/reports/graphql/queries.py b/apps/reports/graphql/queries.py index a8588a6..bfd98c9 100644 --- a/apps/reports/graphql/queries.py +++ b/apps/reports/graphql/queries.py @@ -5,7 +5,7 @@ from main.graphql.permissions import IsAuthenticated from .filters import LinkFilter, ReportFilter, ReportSummaryFilter, ThematicAreaFilter -from .orders import LinkOrder, ReportOrder +from .orders import LinkOrder from .types import LinkType, ReportSummaryType, ReportType, ThematicAreaType @@ -17,7 +17,6 @@ class Query: reports: OffsetPaginated[ReportType] = strawberry_django.offset_paginated( filters=ReportFilter, - order=ReportOrder, ) report: ReportType = strawberry_django.field() diff --git a/apps/reports/tests/queries_test.py b/apps/reports/tests/queries_test.py index 72ecf78..e4237d8 100644 --- a/apps/reports/tests/queries_test.py +++ b/apps/reports/tests/queries_test.py @@ -10,7 +10,7 @@ class TestReportQueries(TestCase): class Query: REPORTS = """ query Reports($pagination: OffsetPaginationInput, $filters: ReportFilter) { - reports(pagination: $pagination, filters: $filters, order: {createdAt: DESC}) { + reports(pagination: $pagination, filters: $filters) { totalCount pageInfo { offset limit } results { diff --git a/schema.graphql b/schema.graphql index 99e33b0..4cfc81d 100644 --- a/schema.graphql +++ b/schema.graphql @@ -892,7 +892,7 @@ type Query { user(id: ID!): UserType! me: UserMeType thematicAreas(pagination: OffsetPaginationInput, filters: ThematicAreaFilter): ThematicAreaTypeOffsetPaginated! - reports(pagination: OffsetPaginationInput, filters: ReportFilter, order: ReportOrder): ReportTypeOffsetPaginated! + reports(pagination: OffsetPaginationInput, filters: ReportFilter): ReportTypeOffsetPaginated! report(id: ID!): ReportType! publicLinks(pagination: OffsetPaginationInput, filters: LinkFilter, order: LinkOrder): LinkTypeOffsetPaginated! internalLinks(pagination: OffsetPaginationInput, filters: LinkFilter, order: LinkOrder): LinkTypeOffsetPaginated! @@ -958,6 +958,7 @@ input ReportFilter { thematicAreaId: ID disasterType: String reportType: ReportTypeEnum + title: StrFilterLookup AND: ReportFilter OR: ReportFilter NOT: ReportFilter @@ -966,12 +967,6 @@ input ReportFilter { regions: [ID!] } -input ReportOrder @oneOf { - title: Ordering - createdAt: Ordering - publishedAt: Ordering -} - """ Tracks the AI extraction lifecycle for a FILE-type Report. @@ -1095,6 +1090,54 @@ enum ReportVisibility { union ResetUserPasswordPayload = UserTypeMutationResponseType | OperationInfo +input StrFilterLookup { + """Exact match. Filter will be skipped on `null` value""" + exact: String + + """Assignment test. Filter will be skipped on `null` value""" + isNull: Boolean + + """ + Exact match of items in a given list. Filter will be skipped on `null` value + """ + inList: [String!] + + """Case-insensitive exact match. Filter will be skipped on `null` value""" + iExact: String + + """ + Case-sensitive containment test. Filter will be skipped on `null` value + """ + contains: String + + """ + Case-insensitive containment test. Filter will be skipped on `null` value + """ + iContains: String + + """Case-sensitive starts-with. Filter will be skipped on `null` value""" + startsWith: String + + """Case-insensitive starts-with. Filter will be skipped on `null` value""" + iStartsWith: String + + """Case-sensitive ends-with. Filter will be skipped on `null` value""" + endsWith: String + + """Case-insensitive ends-with. Filter will be skipped on `null` value""" + iEndsWith: String + + """ + Case-sensitive regular expression match. Filter will be skipped on `null` value + """ + regex: String + + """ + Case-insensitive regular expression match. Filter will be skipped on `null` value + """ + iRegex: String +} + input TeamCreateInput { name: String! description: String From 5cac1dad34921bba1c398a29bc2653c21d7a50ec Mon Sep 17 00:00:00 2001 From: sudip-khanal Date: Tue, 30 Jun 2026 14:06:01 +0545 Subject: [PATCH 2/2] feat(report): make thematic area optional on report --- apps/reports/graphql/inputs.py | 2 +- .../0010_alter_report_thematic_area.py | 19 +++++++++++++++++++ apps/reports/models.py | 2 ++ schema.graphql | 2 +- 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 apps/reports/migrations/0010_alter_report_thematic_area.py diff --git a/apps/reports/graphql/inputs.py b/apps/reports/graphql/inputs.py index 66b2cf2..9e3d3d1 100644 --- a/apps/reports/graphql/inputs.py +++ b/apps/reports/graphql/inputs.py @@ -30,7 +30,7 @@ class ReportCreateInput: iframe_url: str | None = strawberry.UNSET visibility: ReportVisibility = ReportVisibility.PUBLIC report_type: ReportType = ReportType.REPORT - thematic_area: strawberry.ID + thematic_area: strawberry.ID | None = strawberry.UNSET region: strawberry.ID | None = strawberry.UNSET disaster_type: str | None = strawberry.UNSET owner: str | None = strawberry.UNSET diff --git a/apps/reports/migrations/0010_alter_report_thematic_area.py b/apps/reports/migrations/0010_alter_report_thematic_area.py new file mode 100644 index 0000000..b73502c --- /dev/null +++ b/apps/reports/migrations/0010_alter_report_thematic_area.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.3 on 2026-06-30 08:17 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reports', '0009_create_hnsw_index'), + ] + + operations = [ + migrations.AlterField( + model_name='report', + name='thematic_area', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='reports', to='reports.thematicarea'), + ), + ] diff --git a/apps/reports/models.py b/apps/reports/models.py index df22346..377ef08 100644 --- a/apps/reports/models.py +++ b/apps/reports/models.py @@ -92,6 +92,8 @@ class Report(BaseModel): ThematicArea, on_delete=models.PROTECT, related_name="reports", + blank=True, + null=True, ) region = models.ForeignKey( "geo.AdminArea", diff --git a/schema.graphql b/schema.graphql index 4cfc81d..84fdc45 100644 --- a/schema.graphql +++ b/schema.graphql @@ -933,7 +933,7 @@ input ReportCreateInput { iframeUrl: String visibility: ReportVisibility! = PUBLIC reportType: ReportTypeEnum! = REPORT - thematicArea: ID! + thematicArea: ID region: ID disasterType: String owner: String