Skip to content

[Feat] : 검색 결과 정확 일치 우선 정렬 (#184)#185

Merged
GulSam00 merged 1 commit into
developfrom
feat/184-searchExactMatchPriority
Apr 10, 2026
Merged

[Feat] : 검색 결과 정확 일치 우선 정렬 (#184)#185
GulSam00 merged 1 commit into
developfrom
feat/184-searchExactMatchPriority

Conversation

@GulSam00
Copy link
Copy Markdown
Owner

@GulSam00 GulSam00 commented Apr 9, 2026

User description

📌 PR 제목

[Feat] : 검색 결과 정확 일치 우선 정렬

📌 변경 사항

  • 검색 API에 정확 일치(exact match) 우선 정렬 로직 추가
  • 정확 일치 쿼리와 부분 일치 쿼리를 분리하여 정확 일치 결과를 먼저 반환
  • 정확 일치 → 부분 일치 순서로 페이지네이션 처리 (경계 페이지 포함)
  • type=all (제목+가수), type=title, type=artist 모든 검색 타입 지원
  • 인증/비인증 사용자의 중복 쿼리 로직을 executeSearchQueries 함수로 통합
  • applyExactFilter, applyPartialFilter 헬퍼 함수 추출로 가독성 개선

💬 추가 참고 사항


PR Type

Enhancement


Description

  • Implement exact match priority sorting in search results

  • Separate exact and partial match queries for better ranking

  • Handle pagination across exact-partial match boundaries

  • Consolidate duplicate search logic for authenticated/unauthenticated users

  • Extract helper functions for improved code readability


Diagram Walkthrough

flowchart LR
  A["Search Query"] --> B["Count Exact & Partial Matches"]
  B --> C["Determine Page Position"]
  C --> D["Exact Match Results"]
  C --> E["Partial Match Results"]
  D --> F["Combine & Return Results"]
  E --> F
  F --> G["Search Response"]
Loading

File Walkthrough

Relevant files
Enhancement
route.ts
Refactor search API with exact match priority logic           

apps/web/src/app/api/search/route.ts

  • Added applyExactFilter helper function for exact match queries
  • Added applyPartialFilter helper function for partial match queries
    excluding exact matches
  • Created executeSearchQueries function to consolidate search logic with
    exact-match priority
  • Implemented pagination logic that handles boundary cases between exact
    and partial results
  • Unified authenticated and unauthenticated user search paths into
    single flow
  • Simplified response mapping with conditional checks for authenticated
    state
+129/-85

@GulSam00
Copy link
Copy Markdown
Owner Author

GulSam00 commented Apr 9, 2026

/describe

@GulSam00
Copy link
Copy Markdown
Owner Author

GulSam00 commented Apr 9, 2026

/review

@GulSam00
Copy link
Copy Markdown
Owner Author

GulSam00 commented Apr 9, 2026

/improve

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 9, 2026

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

1 similar comment
@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 9, 2026

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

PR Description updated to latest commit (ee48ec6)

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Prioritize exact match results in search API with improved pagination

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Implement exact match priority sorting in search results
• Separate exact and partial match queries with proper pagination
• Extract helper functions for filter logic and query execution
• Unify authenticated and unauthenticated search logic
Diagram
flowchart LR
  A["Search Query"] --> B["Count Exact & Partial Matches"]
  B --> C{Current Page Location}
  C -->|Exact Only| D["Fetch Exact Results"]
  C -->|Partial Only| E["Fetch Partial Results"]
  C -->|Boundary| F["Fetch Exact + Partial"]
  D --> G["Combine & Return"]
  E --> G
  F --> G
  G --> H["Search Results"]
Loading

Grey Divider

File Changes

1. apps/web/src/app/api/search/route.ts ✨ Enhancement +129/-85

Exact match priority search with unified query logic

• Added applyExactFilter and applyPartialFilter helper functions for cleaner query building
• Introduced executeSearchQueries function to handle exact match priority with intelligent
 pagination
• Refactored GET handler to unify authenticated and unauthenticated search logic
• Implemented boundary page handling to seamlessly transition between exact and partial results
• Removed duplicate query logic by consolidating both user types into single execution path

apps/web/src/app/api/search/route.ts


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 9, 2026

Code Review by Qodo

🐞 Bugs (3)   📘 Rule violations (0)   📎 Requirement gaps (0)   🎨 UX Issues (0)
🐞\ ☼ Reliability (1) ⛨ Security (1) ➹ Performance (1)

Grey Divider


Action required

1. OR filter string injection 🐞
Description
applyExactFilter/applyPartialFilter interpolate the user-provided query directly into the .or()
filter string for type=all, so inputs containing PostgREST-delimiter characters (e.g., ,, (,
)) can break the filter syntax or alter the filter logic. This can cause incorrect search results
or request failures (500) for certain queries.
Code

apps/web/src/app/api/search/route.ts[R25-37]

+function applyExactFilter(baseQuery: any, type: string, searchText: string) {
+  if (type === 'all') {
+    return baseQuery.or(`title.ilike.${searchText},artist.ilike.${searchText}`);
+  }
+  return baseQuery.ilike(type, searchText);
+}
+
+function applyPartialFilter(baseQuery: any, type: string, searchText: string) {
+  if (type === 'all') {
+    return baseQuery
+      .or(`title.ilike.%${searchText}%,artist.ilike.%${searchText}%`)
+      .not('title', 'ilike', searchText)
+      .not('artist', 'ilike', searchText);
Evidence
The API reads q from the URL and passes it unescaped into applyExactFilter/applyPartialFilter,
which build PostgREST filter expressions via string interpolation inside .or(...).

apps/web/src/app/api/search/route.ts[25-40]
apps/web/src/app/api/search/route.ts[131-168]
apps/web/src/lib/api/searchSong.ts[6-16]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
For `type=all`, the code constructs PostgREST filter strings with raw user input and passes them to `.or(...)`. This can break parsing or let the user affect the filter expression.

### Issue Context
`q` comes from the request URL and is forwarded by the web client without escaping.

### Fix Focus Areas
- apps/web/src/app/api/search/route.ts[25-40]
- apps/web/src/app/api/search/route.ts[131-168]

### Suggested fix
- Avoid building `.or()` filter strings with raw input. Prefer a server-side RPC (SQL function) that accepts `query` as a parameter and applies `(title ILIKE ...) OR (artist ILIKE ...)` safely.
- If RPC is not an option, implement a strict escaping/validation layer for `q` specifically for PostgREST filter-string contexts (reject or escape `,()`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Client-controlled auth flag 🐞
Description
GET() trusts the authenticated query parameter to decide whether to join/select per-user tables
and whether to call getAuthenticatedUser(), instead of deriving auth from the Supabase session on
the server. A client can flip this flag, causing unnecessary heavier queries (and potentially
different error behavior) independent of the real session state.
Code

apps/web/src/app/api/search/route.ts[R157-176]

+    const selectClause = authenticated
+      ? `*, thumb_logs(*), tosings(user_id), like_activities(user_id), save_activities(user_id)`
+      : `*, thumb_logs(*)`;

-    const { data, error, count } = await baseQuery.order(order).range(from, to);
+    const result = await executeSearchQueries(supabase, selectClause, query, type, order, from, to);

-    if (error) {
+    if ('error' in result) {
      return NextResponse.json(
        {
          success: false,
-          error: error?.message || 'Unknown error',
+          error: result.error,
        },
        { status: 500 },
      );
    }

-    // data를 Song 타입으로 파싱해야 함
-    const songs: SearchSong[] = data.map((song: DBSong) => ({
+    let userId: string | undefined;
+    if (authenticated) {
+      userId = await getAuthenticatedUser(supabase);
+    }
Evidence
The API reads authenticated from the URL and uses it to choose selectClause and whether to fetch
the user via supabase.auth.getUser(). Since the server client has cookie access, it can and should
determine auth from session, not from a client param.

apps/web/src/app/api/search/route.ts[134-176]
apps/web/src/utils/getAuthenticatedUser.ts[3-12]
apps/web/src/lib/supabase/server.ts[5-29]
apps/web/src/lib/api/searchSong.ts[6-16]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The search route uses a client-supplied `authenticated` query param to control server query shape and auth behavior. This is not a reliable trust boundary and can be manipulated.

### Issue Context
The server Supabase client is created with cookie support, so the route can detect session state directly.

### Fix Focus Areas
- apps/web/src/app/api/search/route.ts[134-176]
- apps/web/src/lib/supabase/server.ts[5-29]
- apps/web/src/utils/getAuthenticatedUser.ts[3-12]

### Suggested fix
- Remove (or ignore) the `authenticated` URL parameter.
- Determine auth on the server:
 - Attempt `supabase.auth.getUser()` (or `getAuthenticatedUser`) first.
 - If authenticated, set `userId` and use the authenticated select clause.
 - If not, use the unauthenticated select clause and skip per-user joins.
- Ensure the unauthenticated path never selects personal activity relations.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Heavy extra count queries 🐞
Description
executeSearchQueries always runs two separate count queries (exact + partial) and reuses the full
selectClause (including embedded relations) for those counts. This increases database work per
request and can significantly raise latency under load compared to counting with a minimal select.
Code

apps/web/src/app/api/search/route.ts[R53-68]

+  // 1. 정확 일치 / 부분 일치 각각의 총 개수를 병렬로 조회
+  const exactCountQuery = applyExactFilter(
+    supabase.from('songs').select(selectClause, { count: 'exact', head: true }),
+    type,
+    query,
+  );
+  const partialCountQuery = applyPartialFilter(
+    supabase.from('songs').select(selectClause, { count: 'exact', head: true }),
+    type,
+    query,
+  );
+
+  const [exactCountResult, partialCountResult] = await Promise.all([
+    exactCountQuery,
+    partialCountQuery,
+  ]);
Evidence
The implementation performs two count:'exact' queries for every request and passes selectClause
(which may embed thumb_logs, tosings, like_activities, save_activities) even though counts
don’t need these fields.

apps/web/src/app/api/search/route.ts[42-76]
apps/web/src/app/api/search/route.ts[157-162]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Count queries currently use the same heavy `selectClause` as data queries. This adds unnecessary work for counts and doubles the number of count calls.

### Issue Context
Counts are used only to compute pagination boundaries and `hasNext`.

### Fix Focus Areas
- apps/web/src/app/api/search/route.ts[42-76]
- apps/web/src/app/api/search/route.ts[157-162]

### Suggested fix
- For count queries, use a minimal select (e.g., `select('id', { count: 'exact', head: true })`) instead of `selectClause`.
- Consider eliminating the count queries entirely by fetching at most `(pageSize + 1)` items across the exact+partial composition and computing `hasNext` from whether an extra item exists.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
singcode Ready Ready Preview, Comment Apr 9, 2026 3:24pm

Comment on lines +25 to +37
function applyExactFilter(baseQuery: any, type: string, searchText: string) {
if (type === 'all') {
return baseQuery.or(`title.ilike.${searchText},artist.ilike.${searchText}`);
}
return baseQuery.ilike(type, searchText);
}

function applyPartialFilter(baseQuery: any, type: string, searchText: string) {
if (type === 'all') {
return baseQuery
.or(`title.ilike.%${searchText}%,artist.ilike.%${searchText}%`)
.not('title', 'ilike', searchText)
.not('artist', 'ilike', searchText);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Or filter string injection 🐞 Bug ⛨ Security

applyExactFilter/applyPartialFilter interpolate the user-provided query directly into the .or()
filter string for type=all, so inputs containing PostgREST-delimiter characters (e.g., ,, (,
)) can break the filter syntax or alter the filter logic. This can cause incorrect search results
or request failures (500) for certain queries.
Agent Prompt
### Issue description
For `type=all`, the code constructs PostgREST filter strings with raw user input and passes them to `.or(...)`. This can break parsing or let the user affect the filter expression.

### Issue Context
`q` comes from the request URL and is forwarded by the web client without escaping.

### Fix Focus Areas
- apps/web/src/app/api/search/route.ts[25-40]
- apps/web/src/app/api/search/route.ts[131-168]

### Suggested fix
- Avoid building `.or()` filter strings with raw input. Prefer a server-side RPC (SQL function) that accepts `query` as a parameter and applies `(title ILIKE ...) OR (artist ILIKE ...)` safely.
- If RPC is not an option, implement a strict escaping/validation layer for `q` specifically for PostgREST filter-string contexts (reject or escape `,()`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@GulSam00 GulSam00 merged commit 384324d into develop Apr 10, 2026
2 checks passed
@GulSam00 GulSam00 deleted the feat/184-searchExactMatchPriority branch April 12, 2026 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

검색 결과 정확 일치 우선 정렬

1 participant