Skip to content

BACKEND ISSUE 07 — [FEATURE] Implement Research Landscape Sector Distribution API With Project-Based Filters #14

Description

@LeDuyCoder

🎯 Goal

Implement API GET /analytics/distribution để trả về dữ liệu tỷ lệ phân bổ nghiên cứu phục vụ cho các chart trên dashboard Analytics.

API này dùng cho 2 khối UI chính:

  • Research Landscape: biểu đồ khối hình chữ nhật thể hiện tỷ lệ theo lĩnh vực/ngành nghiên cứu.
  • Impact Quartiles: biểu đồ vòng tròn tỷ lệ thể hiện phân bổ theo nhóm tác động/phân hạng chất lượng ấn phẩm.

API cần hỗ trợ lọc theo project_id.
Từ project_id, hệ thống sẽ lấy các phạm vi nghiên cứu mà project đang theo dõi, ví dụ:

  • subject_area
  • keywords

Sau đó dùng các thông tin này để lọc dữ liệu bài báo trước khi tính tỷ lệ phân bổ.


🖼️ UI Mapping

1. Research Landscape

Dùng để hiển thị tỷ lệ phân bổ theo lĩnh vực/ngành nghiên cứu.

Ví dụ:

[
  { "name": "Biotech", "percentage": 42 },
  { "name": "A.I.", "percentage": 28 },
  { "name": "Materials", "percentage": 15 }
]

2. Impact Quartiles

Dùng để hiển thị tỷ lệ phân bổ theo nhóm tác động hoặc journal quartile.

Ví dụ:

[
  { "name": "Q1", "percentage": 45 },
  { "name": "Q2", "percentage": 30 },
  { "name": "Q3", "percentage": 15 },
  { "name": "Q4", "percentage": 10 }
]

📡 Endpoint

GET /analytics/distribution

📥 Query Parameters

Param Type Required Description
project_id string / uuid Yes ID của project cần lấy dữ liệu distribution
distribution_type string No Loại phân bổ cần lấy. Giá trị đề xuất: sector, impact_quartile
subject_area string No Lọc hẹp thêm theo subject area cụ thể
keywords string[] / comma-separated No Lọc hẹp thêm theo danh sách keyword
from_year number No Năm bắt đầu lọc dữ liệu
to_year number No Năm kết thúc lọc dữ liệu

✅ Example Requests

1. Fetch default distribution by project

GET /analytics/distribution?project_id=PROJECT_ID

2. Fetch Research Landscape sector distribution

GET /analytics/distribution?project_id=PROJECT_ID&distribution_type=sector

3. Fetch Impact Quartiles distribution

GET /analytics/distribution?project_id=PROJECT_ID&distribution_type=impact_quartile

4. Fetch distribution with subject area filter

GET /analytics/distribution?project_id=PROJECT_ID&distribution_type=sector&subject_area=Computer Science

5. Fetch distribution with keywords filter

GET /analytics/distribution?project_id=PROJECT_ID&distribution_type=sector&keywords=AI,Machine Learning,RAG

6. Fetch distribution with year range

GET /analytics/distribution?project_id=PROJECT_ID&distribution_type=impact_quartile&from_year=2021&to_year=2026

🧾 Response Contract

{
  "code": 200,
  "message": "Fetch distribution successfully",
  "data": [
    {
      "name": "Biotech",
      "percentage": 42
    },
    {
      "name": "A.I.",
      "percentage": 28
    },
    {
      "name": "Materials",
      "percentage": 15
    }
  ]
}

🧠 Business Logic

1. Project-based filtering

API bắt buộc nhận project_id.

Từ project_id, hệ thống cần lấy các scope nghiên cứu mà project đang theo dõi:

  • Subject areas
  • Keywords

Sau đó dùng scope này để lọc dataset bài báo toàn cầu.

Ví dụ project đang theo dõi:

{
  "project_id": "P001",
  "subject_areas": ["Artificial Intelligence", "Data Science"],
  "keywords": ["LLM", "RAG", "Machine Learning"]
}

API chỉ tính distribution dựa trên các bài báo match với subject area hoặc keywords của project.


2. Distribution type

API cần hỗ trợ ít nhất 2 loại distribution:

distribution_type Purpose UI
sector Phân bổ theo lĩnh vực/ngành nghiên cứu Research Landscape
impact_quartile Phân bổ theo nhóm tác động hoặc journal quartile Impact Quartiles

Nếu client không truyền distribution_type, mặc định dùng:

sector

3. Filter priority

Nếu client chỉ truyền:

project_id

API tự động lấy toàn bộ subject_areakeywords của project để lọc.

Nếu client truyền thêm:

subject_area
keywords

API sẽ lọc hẹp hơn trong phạm vi project.

Ví dụ:

GET /analytics/distribution?project_id=1&distribution_type=sector&subject_area=AI&keywords=LLM,RAG

Kết quả chỉ tính những bài báo:

  • Thuộc phạm vi project 1
  • Thuộc subject area AI
  • Có keyword liên quan đến LLM hoặc RAG

4. Percentage calculation

Sau khi filter dữ liệu, API cần group dữ liệu theo distribution_type.

Với sector

Group theo lĩnh vực/ngành nghiên cứu.

Ví dụ field có thể dùng:

subject_area
sector
category

Với impact_quartile

Group theo nhóm tác động hoặc journal quartile.

Ví dụ field có thể dùng:

quartile
impact_quartile
journal_quartile

5. Percentage formula

Công thức tính:

percentage = round((group_count / total_count) * 100)

Yêu cầu:

  • Tổng percentage nên xấp xỉ 100
  • Không có null
  • Không có NaN
  • Nếu cần làm tròn, item cuối có thể được điều chỉnh để tổng chính xác bằng 100

Ví dụ:

[
  { "name": "A.I.", "percentage": 50 },
  { "name": "Biotech", "percentage": 30 },
  { "name": "Materials", "percentage": 20 }
]

6. Sorting rule

Response nên sort giảm dần theo percentage.

Ví dụ:

[
  { "name": "Biotech", "percentage": 42 },
  { "name": "A.I.", "percentage": 28 },
  { "name": "Materials", "percentage": 15 }
]

📦 Data Source - Phase 1

Trong phase 1, dùng mock dataset in-memory.

Mock dataset nên có field tối thiểu:

{
  article_id: string;
  year: number;
  subject_area: string;
  sector: string;
  keywords: string[];
  impact_quartile: string;
}

Hoặc nếu mock theo aggregate sẵn:

{
  name: string;
  count: number;
  distribution_type: "sector" | "impact_quartile";
  subject_area: string;
  keywords: string[];
  year: number;
}

⚠️ Edge Cases

1. Project not found

Nếu project_id không tồn tại:

{
  "code": 404,
  "message": "Project not found",
  "data": null
}

2. Project has no subject area / keywords

Nếu project chưa có scope theo dõi:

{
  "code": 200,
  "message": "Fetch distribution successfully",
  "data": []
}

3. Empty filtered dataset

Nếu filter xong không có bài báo phù hợp:

{
  "code": 200,
  "message": "Fetch distribution successfully",
  "data": []
}

4. Invalid distribution_type

Nếu distribution_type không hợp lệ:

{
  "code": 400,
  "message": "Invalid distribution type",
  "data": null
}

Allowed values:

sector
impact_quartile

5. Invalid year range

Nếu from_year > to_year:

{
  "code": 400,
  "message": "Invalid year range",
  "data": null
}

6. Missing group value

Nếu record thiếu field để group, ví dụ thiếu sector hoặc impact_quartile:

  • Không làm API lỗi
  • Có thể bỏ qua record đó
  • Không trả về item có name: null

🧪 Acceptance Criteria

  • API nhận được project_id
  • API tự lấy được subject_areakeywords mà project đang theo dõi
  • API hỗ trợ distribution_type=sector
  • API hỗ trợ distribution_type=impact_quartile
  • Nếu không truyền distribution_type, mặc định dùng sector
  • API hỗ trợ filter thêm bằng subject_area
  • API hỗ trợ filter thêm bằng keywords
  • API hỗ trợ filter theo from_yearto_year
  • Response trả về danh sách distribution metrics
  • Mỗi item có đủ namepercentage
  • percentage luôn là number
  • Không có giá trị null trong response
  • Không có NaN trong response
  • Response sort giảm dần theo percentage
  • Empty dataset trả về data: [], không làm API lỗi
  • FE có thể render Research Landscape trực tiếp không cần transform thêm
  • FE có thể render Impact Quartiles trực tiếp không cần transform thêm
  • Response time với mock dataset dưới 100ms

🧪 Test Cases

TC-01: Fetch default distribution by project_id only

Request

GET /analytics/distribution?project_id=1

Expected

  • API lấy subject areas và keywords của project
  • Mặc định dùng distribution_type=sector
  • Filter dữ liệu theo project scope
  • Tính percentage theo sector
  • Response sort giảm dần theo percentage

TC-02: Fetch sector distribution

Request

GET /analytics/distribution?project_id=1&distribution_type=sector

Expected

  • Group dữ liệu theo sector hoặc subject area
  • Mỗi item có namepercentage
  • Tổng percentage xấp xỉ 100

TC-03: Fetch impact quartile distribution

Request

GET /analytics/distribution?project_id=1&distribution_type=impact_quartile

Expected

  • Group dữ liệu theo impact quartile hoặc journal quartile
  • Ví dụ: Q1, Q2, Q3, Q4
  • FE render được vòng tròn tỷ lệ trực tiếp

TC-04: Fetch distribution with subject_area filter

Request

GET /analytics/distribution?project_id=1&distribution_type=sector&subject_area=Artificial Intelligence

Expected

  • Chỉ lấy dữ liệu thuộc subject area được truyền vào
  • Không trả về dữ liệu ngoài scope project
  • Data vẫn tính percentage đúng

TC-05: Fetch distribution with keywords filter

Request

GET /analytics/distribution?project_id=1&distribution_type=sector&keywords=LLM,RAG

Expected

  • Chỉ lấy dữ liệu có keyword match với LLM hoặc RAG
  • Không có item null
  • Không có percentage null hoặc NaN

TC-06: Fetch distribution with year range

Request

GET /analytics/distribution?project_id=1&distribution_type=impact_quartile&from_year=2021&to_year=2026

Expected

  • Chỉ lấy dữ liệu trong khoảng năm 2021 đến 2026
  • Group theo impact quartile sau khi filter
  • Không trả về data ngoài khoảng năm

TC-07: Project not found

Request

GET /analytics/distribution?project_id=999

Expected

{
  "code": 404,
  "message": "Project not found",
  "data": null
}

TC-08: Empty filtered dataset

Request

GET /analytics/distribution?project_id=1&keywords=unknown-keyword

Expected

{
  "code": 200,
  "message": "Fetch distribution successfully",
  "data": []
}

TC-09: Invalid distribution_type

Request

GET /analytics/distribution?project_id=1&distribution_type=unknown

Expected

{
  "code": 400,
  "message": "Invalid distribution type",
  "data": null
}

TC-10: Missing group value in mock data

Input mock data

[
  {
    "article_id": "A001",
    "year": 2024,
    "subject_area": "Artificial Intelligence",
    "sector": "A.I.",
    "keywords": ["LLM"],
    "impact_quartile": "Q1"
  },
  {
    "article_id": "A002",
    "year": 2024,
    "subject_area": "Artificial Intelligence",
    "sector": null,
    "keywords": ["RAG"],
    "impact_quartile": "Q2"
  }
]

Expected với distribution_type=sector

{
  "code": 200,
  "message": "Fetch distribution successfully",
  "data": [
    {
      "name": "A.I.",
      "percentage": 100
    }
  ]
}

📌 Implementation Notes

Nên tách logic thành các phần riêng:

Controller
  -> validate query params
Service
  -> get project tracking scope
  -> filter distribution dataset
  -> group by selected distribution type
  -> calculate percentage
  -> normalize percentage
  -> build response contract

Gợi ý function:

getProjectTrackingScope(projectId)
filterDistributionDataset(dataset, filters)
groupDistributionData(filteredDataset, distributionType)
calculateDistributionPercentage(groupedData)
normalizePercentage(distributionItems)
buildDistributionResponse(distributionItems)

📦 Suggested Mock Data

const mockDistributionData = [
  {
    article_id: "A001",
    year: 2024,
    subject_area: "Artificial Intelligence",
    sector: "A.I.",
    keywords: ["LLM", "Machine Learning"],
    impact_quartile: "Q1"
  },
  {
    article_id: "A002",
    year: 2024,
    subject_area: "Biotechnology",
    sector: "Biotech",
    keywords: ["Genomics"],
    impact_quartile: "Q2"
  },
  {
    article_id: "A003",
    year: 2025,
    subject_area: "Materials Science",
    sector: "Materials",
    keywords: ["Nanomaterials"],
    impact_quartile: "Q3"
  }
];

🚀 Future Improvements

  • Thay mock dataset bằng DB aggregation
  • Thêm distribution_type=country
  • Thêm distribution_type=institution
  • Thêm distribution_type=publisher
  • Thêm filter theo region/country
  • Cho FE truyền limit để lấy top N nhóm lớn nhất
  • Cache response bằng Redis theo key:
analytics:distribution:{project_id}:{distribution_type}:{subject_area}:{keywords}:{from_year}:{to_year}
  • Tối ưu query bằng index cho:
    • project_id
    • year
    • subject_area
    • keyword
    • sector
    • impact_quartile

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

Status
Todo

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions