Skip to content

BACKEND ISSUE 13 — [FEATURE] Implement Topic Intensity Matrix API With Project-Based Filters #29

Description

@LeDuyCoder

🎯 Goal

Implement API GET /analytics/matrix/intensity để trả về dữ liệu cho bảng heatmap Topic Intensity Matrix.

API này dùng để biểu thị mức độ tập trung nghiên cứu của từng tác giả hoặc tổ chức vào từng chủ đề nghiên cứu.

Biểu đồ sẽ giúp FE render dạng:

Rows: Authors / Institutions
Columns: Topics
Cells: Intensity score

Trong đó intensity là giá trị từ 0 đến 1, dùng để quyết định độ đậm nhạt của ô màu cam trên heatmap.


🖼️ UI Mapping

Khối UI:

Topic Intensity Matrix

Chart type:

Heatmap / Matrix table / Color grid

Visual style:

  • Mỗi row đại diện cho một tác giả hoặc tổ chức
  • Mỗi column đại diện cho một topic
  • Mỗi cell đại diện cho mức độ tập trung nghiên cứu
  • Ô có intensity càng cao thì màu cam càng đậm
  • Ô có intensity thấp thì màu cam nhạt hoặc gần trắng

Example:

             AI/ML    BIO-MED    PHYSICS
H. Vance     0.9      0.2        0.1
M. Lee       0.4      0.8        0.3

📡 Endpoint

GET /analytics/matrix/intensity

📥 Query Parameters

Param Type Required Description
project_id string / number Yes ID của project cần lấy topic intensity matrix
row_type string No Loại row muốn hiển thị. Values: author, institution. Default: author
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
limit_rows number No Số lượng row tối đa. Default: 10
limit_topics number No Số lượng topic tối đa. Default: 8

✅ Example Requests

1. Fetch author-topic intensity matrix by project only

GET /analytics/matrix/intensity?project_id=PROJECT_ID

2. Fetch institution-topic intensity matrix

GET /analytics/matrix/intensity?project_id=PROJECT_ID&row_type=institution

3. Fetch matrix with subject area filter

GET /analytics/matrix/intensity?project_id=PROJECT_ID&subject_area=Computer Science

4. Fetch matrix with keywords filter

GET /analytics/matrix/intensity?project_id=PROJECT_ID&keywords=AI,Machine Learning,RAG

5. Fetch matrix with year range

GET /analytics/matrix/intensity?project_id=PROJECT_ID&from_year=2021&to_year=2026

6. Fetch matrix with row and topic limits

GET /analytics/matrix/intensity?project_id=PROJECT_ID&limit_rows=12&limit_topics=6

🧾 Response Contract

{
  "code": 200,
  "message": "Fetch topic intensity matrix successfully",
  "data": [
    {
      "rowName": "H. Vance",
      "topic": "AI/ML",
      "intensity": 0.9
    },
    {
      "rowName": "H. Vance",
      "topic": "BIO-MED",
      "intensity": 0.2
    }
  ]
}

📌 Response Field Explanation

Field Type Description
rowName string Tên tác giả hoặc tổ chức
topic string Tên topic / subject category / research area
intensity number Mức độ tập trung nghiên cứu, từ 0 đến 1

🧠 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 scope nghiên cứu của project:

  • Subject area của project
  • Subject categories thuộc subject area đó
  • Keywords project đang theo dõi

Sau đó dùng scope này để lọc tập bài báo trước khi tính topic intensity.

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

{
  "project_id": 1,
  "subject_area": "Artificial Intelligence",
  "subject_categories": ["Computer Science Applications", "Artificial Intelligence"],
  "keywords": ["LLM", "RAG", "Machine Learning"]
}

API chỉ tính matrix trên các bài báo thuộc scope này.


2. Filter priority

Nếu client chỉ truyền:

project_id

API tự động lấy toàn bộ subject area, subject categories và keywords 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/matrix/intensity?project_id=1&subject_area=AI&keywords=LLM,RAG

Kết quả chỉ tính topic intensity trên các bài báo:

  • Thuộc project scope
  • Thuộc subject area AI
  • Có keyword liên quan đến LLM hoặc RAG

3. Row type logic

API hỗ trợ 2 loại row:

author
institution

row_type = author

Mỗi row là một tác giả.

{
  "rowName": "H. Vance",
  "topic": "AI/ML",
  "intensity": 0.9
}

Dữ liệu lấy từ:

Author -> Author_Article -> Article -> Topic

row_type = institution

Mỗi row là một tổ chức / viện nghiên cứu / trường đại học.

{
  "rowName": "Stanford Bio-Dynamics Lab",
  "topic": "AI/ML",
  "intensity": 0.9
}

Dữ liệu lấy từ affiliation / institution của author hoặc article.

Important: Institution không phải Publisher hoặc Journal. Institution nên đến từ dữ liệu affiliation / organization.

Nếu chưa có Institution/Affiliation data, phase 1 có thể:

  • Chỉ hỗ trợ row_type=author
  • Với row_type=institution, trả data: []
  • Tạo follow-up task để bổ sung Institution/Affiliation schema

4. Topic source

Topic có thể lấy từ:

Topic.display_name

hoặc nếu UI muốn nhóm lớn hơn:

Subject_Category.display_name

Suggested phase 1:

topic = Topic.display_name

Nếu topic quá nhiều, API lấy top topics theo article count hoặc citation count trong project scope.


5. Intensity calculation

intensity thể hiện mức độ tập trung nghiên cứu của một row vào một topic.

Suggested formula:

intensity = topicArticleCountOfRow / maxTopicArticleCountOfThatRow

Trong đó:

Metric Meaning
topicArticleCountOfRow Số bài báo của row trong topic đó
maxTopicArticleCountOfThatRow Số bài báo cao nhất của row trong một topic bất kỳ

Ví dụ:

Author H. Vance:
AI/ML = 9 articles
BIO-MED = 2 articles
PHYSICS = 1 article

max = 9

AI/ML intensity = 9 / 9 = 1.0
BIO-MED intensity = 2 / 9 = 0.22
PHYSICS intensity = 1 / 9 = 0.11

Kết quả có thể round:

round to 2 decimal places

6. Alternative global normalization

Nếu FE muốn so sánh cường độ giữa toàn bộ authors/institutions thay vì trong từng row, có thể dùng global normalization:

intensity = topicArticleCountOfRow / maxTopicArticleCountAcrossAllRows

Suggested phase 1:

Use row-based normalization

Vì row-based normalization giúp FE dễ thấy mỗi tác giả/tổ chức tập trung mạnh vào topic nào.


7. Missing cells

Để FE render heatmap ổn định, API nên trả đủ combination giữa selected rows và selected topics.

Nếu một row không có bài trong topic đó:

{
  "rowName": "H. Vance",
  "topic": "PHYSICS",
  "intensity": 0
}

Rule:

Every selected row should have one item for every selected topic.

Ví dụ có 3 rows và 4 topics:

data.length = 3 * 4 = 12

8. Sorting rule

Suggested sorting:

Rows

Sort rows theo tổng số bài báo trong project scope giảm dần:

ORDER BY totalArticles DESC

Topics

Sort topics theo tổng số bài báo trong project scope giảm dần:

ORDER BY totalArticles DESC

Sau đó apply:

limit_rows
limit_topics

📦 Data Source

API có thể lấy dữ liệu từ các bảng hiện tại:

Author
Author_Article
Article
Topic
Sub_Topic
Keyword
Keyword_Article
Subject_Category
Subject_Area
Project
Project_Keyword

Relevant schema flow:

Project.subject_area -> Subject_Area.subject_area_id
Subject_Category.subject_area_id -> Subject_Area.subject_area_id
Article.primary_topic -> Topic.topic_id
Topic.subject_category_id -> Subject_Category.subject_category_id
Sub_Topic.article_id -> Article.article_id
Sub_Topic.topic_id -> Topic.topic_id
Author_Article.author_id -> Author.author_id
Author_Article.article_id -> Article.article_id
Keyword_Article.article_id -> Article.article_id
Keyword_Article.keyword_id -> Keyword.keyword_id
Project_Keyword.keyword_id -> Keyword.keyword_id

🧮 Suggested Matrix Construction Logic

1. Resolve project scope

project_id
-> subject_area
-> subject_category_ids
-> keyword_ids

2. Filter related articles

Article match nếu thỏa mãn ít nhất một trong các điều kiện:

Article.primary_topic thuộc subject_category_ids
OR Article có Sub_Topic thuộc subject_category_ids
OR Article có Keyword_Article thuộc keyword_ids

3. Select top rows

Nếu row_type=author:

Group by Author
Sort by total article count DESC
Limit by limit_rows

Nếu row_type=institution:

Group by Institution
Sort by total article count DESC
Limit by limit_rows

4. Select top topics

Group by Topic
Sort by total article count DESC
Limit by limit_topics

5. Build full matrix

Tạo full combination:

selectedRows x selectedTopics

Mỗi cell:

count articles by row and topic
normalize to intensity 0-1

Nếu không có article:

intensity = 0

⚠️ 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 scope

Nếu project chưa có subject area hoặc keywords:

{
  "code": 200,
  "message": "Fetch topic intensity matrix successfully",
  "data": []
}

3. Empty filtered dataset

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

{
  "code": 200,
  "message": "Fetch topic intensity matrix successfully",
  "data": []
}

4. Author has no display name

Nếu author thiếu tên:

rowName = "Unknown Author"

5. Institution data unavailable

Nếu row_type=institution nhưng chưa có Institution/Affiliation data:

{
  "code": 200,
  "message": "Fetch topic intensity matrix successfully",
  "data": []
}

6. Invalid row_type

Nếu row_type không thuộc:

author
institution

Response:

{
  "code": 400,
  "message": "Invalid row_type",
  "data": null
}

7. Invalid year range

Nếu from_year > to_year:

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

8. Invalid limit_rows

Nếu limit_rows <= 0 hoặc không phải number:

{
  "code": 400,
  "message": "Invalid limit_rows",
  "data": null
}

Suggested max:

50

9. Invalid limit_topics

Nếu limit_topics <= 0 hoặc không phải number:

{
  "code": 400,
  "message": "Invalid limit_topics",
  "data": null
}

Suggested max:

30

🧪 Acceptance Criteria

  • API nhận được project_id
  • API tự lấy được subject area / subject categories / keywords của project
  • API hỗ trợ row_type=author
  • API hỗ trợ row_type=institution nếu có institution data
  • 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
  • API hỗ trợ limit_rows
  • API hỗ trợ limit_topics
  • Response trả về array data
  • Mỗi item có đủ rowName, topic, intensity
  • rowName không null
  • topic không null
  • intensity luôn là number
  • intensity nằm trong khoảng 0 đến 1
  • Không có NaN trong response
  • Không có giá trị âm trong response
  • API trả đủ combination giữa selected rows và selected topics
  • Empty dataset trả về array rỗng, không làm API lỗi
  • FE có thể render heatmap trực tiếp không cần transform quá nhiều
  • Response time với dữ liệu nhỏ dưới 100ms

🧪 Test Cases

TC-01: Fetch author-topic intensity matrix by project_id only

Request

GET /analytics/matrix/intensity?project_id=1

Expected

  • API lấy project scope
  • Filter bài báo theo project scope
  • Chọn top authors
  • Chọn top topics
  • Build full heatmap matrix
  • Response đúng contract

TC-02: Fetch institution-topic intensity matrix

Request

GET /analytics/matrix/intensity?project_id=1&row_type=institution

Expected

Nếu có institution data:

  • Mỗi row là institution
  • Mỗi column là topic
  • intensity nằm trong khoảng 0 đến 1

Nếu chưa có institution data:

{
  "code": 200,
  "message": "Fetch topic intensity matrix successfully",
  "data": []
}

TC-03: Fetch matrix with subject_area filter

Request

GET /analytics/matrix/intensity?project_id=1&subject_area=Artificial Intelligence

Expected

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

TC-04: Fetch matrix with keywords filter

Request

GET /analytics/matrix/intensity?project_id=1&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ó intensity null hoặc NaN

TC-05: Fetch matrix with year range

Request

GET /analytics/matrix/intensity?project_id=1&from_year=2021&to_year=2026

Expected

  • Chỉ lấy bài báo trong khoảng năm 2021 đến 2026
  • Không trả data ngoài khoảng năm
  • Response đúng contract

TC-06: Full combination matrix

Input

Selected rows:

H. Vance
M. Lee

Selected topics:

AI/ML
BIO-MED
PHYSICS

Expected

Response phải có:

2 rows * 3 topics = 6 items

Nếu H. Vance không có bài trong PHYSICS:

{
  "rowName": "H. Vance",
  "topic": "PHYSICS",
  "intensity": 0
}

TC-07: Intensity normalization

Input

Author H. Vance có:

AI/ML = 9 articles
BIO-MED = 2 articles
PHYSICS = 1 article

Expected

[
  {
    "rowName": "H. Vance",
    "topic": "AI/ML",
    "intensity": 1
  },
  {
    "rowName": "H. Vance",
    "topic": "BIO-MED",
    "intensity": 0.22
  },
  {
    "rowName": "H. Vance",
    "topic": "PHYSICS",
    "intensity": 0.11
  }
]

TC-08: Project not found

Request

GET /analytics/matrix/intensity?project_id=999

Expected

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

TC-09: Empty filtered dataset

Request

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

Expected

{
  "code": 200,
  "message": "Fetch topic intensity matrix successfully",
  "data": []
}

TC-10: Invalid row_type

Request

GET /analytics/matrix/intensity?project_id=1&row_type=publisher

Expected

{
  "code": 400,
  "message": "Invalid row_type",
  "data": null
}

TC-11: Invalid year range

Request

GET /analytics/matrix/intensity?project_id=1&from_year=2026&to_year=2021

Expected

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

TC-12: Invalid limit_rows

Request

GET /analytics/matrix/intensity?project_id=1&limit_rows=-1

Expected

{
  "code": 400,
  "message": "Invalid limit_rows",
  "data": null
}

TC-13: Invalid limit_topics

Request

GET /analytics/matrix/intensity?project_id=1&limit_topics=-1

Expected

{
  "code": 400,
  "message": "Invalid limit_topics",
  "data": null
}

📌 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 related articles
  -> select top rows
  -> select top topics
  -> build matrix cells
  -> normalize intensity
  -> build response contract

Gợi ý function:

getProjectTrackingScope(projectId)
buildArticleScopeFilter(scope, queryParams)
getTopMatrixRows(rowType, filters, limitRows)
getTopMatrixTopics(filters, limitTopics)
getTopicCountsByRow(rowType, filters)
buildFullMatrix(rows, topics, counts)
normalizeIntensity(matrixItems)
buildTopicIntensityMatrixResponse(matrixItems)

📦 Suggested Mock Data

const mockTopicIntensityMatrix = [
  {
    rowName: "H. Vance",
    topic: "AI/ML",
    intensity: 0.9
  },
  {
    rowName: "H. Vance",
    topic: "BIO-MED",
    intensity: 0.2
  },
  {
    rowName: "M. Lee",
    topic: "AI/ML",
    intensity: 0.4
  },
  {
    rowName: "M. Lee",
    topic: "BIO-MED",
    intensity: 0.8
  }
];

🚀 Future Improvements

  • Add rowId to response
  • Add rowType to response
  • Add topicId to response
  • Add rawCount to response
  • Add normalization=row|global
  • Add metric_type=article_count|citation_count|impact_score
  • Add support for institution rows after Institution/Affiliation schema is available
  • Add tooltip metadata for FE
  • Add Redis caching:
analytics:matrix:intensity:{project_id}:{row_type}:{subject_area}:{keywords}:{from_year}:{to_year}:{limit_rows}:{limit_topics}
  • Optimize query with indexes on:
    • project_id
    • publication_year
    • author_id
    • article_id
    • keyword_id
    • topic_id
    • subject_category_id

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