Skip to content

[REFAC#148] LinkDiscovery ArticleURLPattern optional + 사이트 rule all-pass 전환 - #150

Merged
juhy0987 merged 7 commits into
mainfrom
refactor/#148/optional-article-url-pattern
Apr 29, 2026
Merged

juhy0987 merged 7 commits into
mainfrom
refactor/#148/optional-article-url-pattern

Conversation

@juhy0987

@juhy0987 juhy0987 commented Apr 29, 2026

Copy link
Copy Markdown
Member

연관 이슈

이슈 #100 / #139 후속. 사이트별 "기사 URL" regex 강제로 인해 누락되던 비-article 컨텐츠
(공지/이벤트/시리즈/오피니언/멀티미디어 등) 를 모두 수집하도록 LinkDiscovery 의 pattern 을 optional 로 강등.


구현 내용

1. PageLinkDiscovery 컴포넌트 — pattern optional

  • internal/crawler/parser/rule/discovery.go
  • cfg.ArticleURLPattern 빈 문자열 시 regex compile/매칭 단계 skip
  • pkg/links.Extractor 의 SameOriginOnly / PathPrefixes / ExcludePatterns / MaxLinksPerPage 만으로 필터
  • 0건 매칭 메시지를 모드별 분기 (pattern stale vs page-empty)

2. rule.Parser ParseLinks 분기 완화

  • internal/crawler/parser/rule/parser.go
  • 기존: cfg != nil && cfg.ArticleURLPattern != \"\" → discovery
  • 신규: cfg != nil → discovery (빈 pattern 도 all-pass 모드)
  • ItemContainer fallback 은 LinkDiscovery 객체 자체가 nil 일 때만

3. Migration 008 — 운영 사이트 rules all-pass 전환

  • naver/daum/yonhap/cnn × list rule = 4 rows UPDATE
  • article_url_pattern 빈 문자열로 갱신
  • exclude_patterns 강화 — 사이트별 광고/공유/네비/미디어/about/login 등 노이즈 컷
  • max_links_per_page=200, same_origin_only site profile 유지

5. MaxLinksPerPage 우선순위 정책 (추가 commit)

사용자 피드백 반영 — same-origin 우선 + cross-origin random sample 로 변경:

  1. same-origin (raw.URL host 와 동일) 링크는 maxOut 무시하고 모두 통과
  2. cross-origin 은 잔여 슬롯 (maxOut - len(same)) 만큼 math/rand/v2.Shuffle 로 무작위 sample
  3. maxOut == 0 (무제한) 이면 cross 도 모두 통과

이유:

  • 사이트 자체 컨텐츠 (same) 는 noise 적고 가치 높음 — 모두 발행
  • 외부 링크 (cross) 는 광고/제휴 noise 많음 — cap 통제 + 무작위 sample 로 특정 영역 (페이지 상단 sponsored slot 등) 편향 회피

4. 테스트 갱신 (3건)

  • TestPageLinkDiscovery_EmptyPattern_AllPass — 빈 pattern 으로 모든 same-origin 링크 통과 + 기본 제외 (login 등) 차단 검증
  • TestPageLinkDiscovery_EmptyPattern_ExcludeStillApplies — all-pass 모드에서 ExcludePatterns 동작
  • TestParser_ParseLinks_LinkDiscoveryWithEmptyPattern_AllPassDiscovery — 객체만 있고 pattern 비었을 때 all-pass discovery 진입 (ItemContainer fallback X)
  • 기존 TestPageLinkDiscovery_EmptyPattern_ReturnsEmptySelector 는 의미 변경에 따라 위 케이스로 대체

CI / 머지 게이트 점검

변경 영향 범위

Required Status Checks

  • Commit Lint
  • PR Title Lint
  • Linked Issue Check
  • Format Check
  • Build
  • Test
  • Lint

배포 순서

  1. migration 008 적용 (production DB 의 4개 list rule UPDATE)
  2. 코드 deploy
  3. 라이브 모니터링:
    • 카테고리 페이지의 chained article jobs published 에서 url_count 증가 추이
    • validator reject 비율 — noise 흡수 여부
    • Kafka backlog — issuetracker.crawl.normal 토픽 lag

롤백 계획

  1. 코드 revert: git revert (4 commits, 역순)
  2. migration 008 down 적용 — 007 의 좁은 ArticleURLPattern 으로 복원
  3. 즉시 이전 동작 회귀 (rule cache TTL 5분 이내 자동 적용)

후속 작업 (별도 PR)


Summary by CodeRabbit

  • Bug Fixes

    • Improved link discovery mechanism to support all-pass filtering mode, allowing exclusion patterns to work independently of URL pattern matching.
  • Chores

    • Updated parsing rules for Naver, Daum, Yonhap, and CNN to use relaxed link discovery patterns with enhanced exclusion-based filtering.

Copilot AI review requested due to automatic review settings April 29, 2026 08:27
@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@juhy0987 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 46 minutes and 36 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bb4e3ef2-f37b-42be-a88e-e60e23074639

📥 Commits

Reviewing files that changed from the base of the PR and between 4cfc9a4 and fc95aec.

📒 Files selected for processing (4)
  • internal/crawler/parser/rule/discovery.go
  • internal/storage/parsing_rule.go
  • migrations/up/008_relax_link_discovery_patterns.sql
  • test/internal/parser/rule/discovery_test.go
📝 Walkthrough

Walkthrough

Make ArticleURLPattern optional in LinkDiscovery by allowing empty string patterns, enabling collection of all links on a page with filtering via exclude patterns. Code paths updated to treat LinkDiscovery presence as the sole condition for discovery routing, removing the pattern-emptiness check.

Changes

Cohort / File(s) Summary
Rule Discovery Logic
internal/crawler/parser/rule/discovery.go, internal/crawler/parser/rule/parser.go
Modified to conditionally compile and apply regex filtering only when ArticleURLPattern is non-empty; empty pattern triggers "all-pass" discovery mode. Parser now routes to discovery based solely on LinkDiscovery presence, not pattern value.
Database Migrations
migrations/up/008_relax_link_discovery_patterns.sql, migrations/down/008_relax_link_discovery_patterns.sql
Migration 008 sets article_url_pattern to empty string for four news hosts (naver, daum, yonhap, cnn) and configures site-specific exclude_patterns, max_links_per_page (200), and same_origin_only flags; down migration reverts changes.
Test Updates
test/internal/parser/rule/discovery_test.go
Updated discovery tests to verify "all-pass" mode behavior when ArticleURLPattern is empty; added assertions for ExcludePatterns, SameOriginOnly, and MaxLinksPerPage enforcement; modified parser routing expectations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 A rabbit hops through patterns wild and free,
No rigid URLs to constrain what we see,
All links now welcome on every page,
Exclude the noise—let truth engage! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title references REFAC#148 and describes the main change: making ArticleURLPattern optional and migrating site rules to all-pass mode.
Linked Issues check ✅ Passed All code requirements from #148 are met: ArticleURLPattern is optional, discovery.go skips regex when empty, parser.go calls discovery when LinkDiscovery exists, migration 008 updates four site rules with empty patterns and enhanced exclude_patterns, and tests validate all-pass behavior.
Out of Scope Changes check ✅ Passed All changes directly support issue #148 objectives: code modifications enable optional ArticleURLPattern, migrations implement all-pass mode for four sites, and tests verify new behavior. No unrelated changes detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/#148/optional-article-url-pattern

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 46 minutes and 36 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request implements an 'all-pass' mode for link discovery, enabling the crawler to capture all links when ArticleURLPattern is empty. It includes logic updates to skip regex filtering, refined exclusion patterns in database migrations for major news sites, and updated unit tests. Feedback indicates that related documentation in LinkDiscoveryConfig should be updated to reflect these changes and that a comment in the migration script incorrectly describes the same_origin_only setting.

Comment thread internal/crawler/parser/rule/parser.go
Comment thread migrations/up/008_relax_link_discovery_patterns.sql Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/crawler/parser/rule/parser.go (1)

163-170: ⚠️ Potential issue | 🟡 Minor

Update the fallback error text to match the new routing.

This branch now runs only when LinkDiscovery is nil, so pointing operators at LinkDiscovery.ArticleURLPattern is misleading. If they hit this error, they need the whole link_discovery block, not just the pattern field.

Proposed text tweak
-			Message:    "list rule missing required ItemContainer or ItemLink selector (or set LinkDiscovery.ArticleURLPattern)",
+			Message:    "list rule missing required ItemContainer or ItemLink selector (or set LinkDiscovery)",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/crawler/parser/rule/parser.go` around lines 163 - 170, The error
message returned when both rule.Selectors.ItemContainer and
rule.Selectors.ItemLink are missing (the Error with Code ErrEmptySelector)
incorrectly points users to LinkDiscovery.ArticleURLPattern even though this
branch only runs when LinkDiscovery is nil; update the Error.Message text to
instruct operators to provide the full link_discovery block (not just
ArticleURLPattern) so it accurately reflects the required configuration when
LinkDiscovery is absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/internal/parser/rule/discovery_test.go`:
- Around line 308-324: The test must prove ParseLinks used LinkDiscovery rather
than falling back to ItemContainer: modify
TestParser_ParseLinks_LinkDiscoveryWithEmptyPattern_AllPassDiscovery so the
fakeRepo or repo spy records/guards ItemContainer calls (e.g., have fakeRepo's
ItemContainer method panic or set a flag if invoked) and assert it was not
called after calling rule.NewParser(...).ParseLinks; alternatively make
r.Selectors.ItemContainer an impossible selector that cannot match listHTML so
only LinkDiscovery can produce the three links. Ensure you reference ParseLinks,
TestParser_ParseLinks_LinkDiscoveryWithEmptyPattern_AllPassDiscovery,
LinkDiscoveryConfig, fakeRepo and ItemContainer in the change.
- Around line 186-229: Extend the empty-pattern tests to assert that
PathPrefixes still filter links when ArticleURLPattern is disabled: in
TestPageLinkDiscovery_EmptyPattern_ExcludeStillApplies (or add a new similar
test) set cfg.PathPrefixes (e.g., []string{"/article/", "/news/"}) along with
ArticleURLPattern:"" and SameOriginOnly:true, call d.Discover(makeRaw(...), cfg)
and then assert that no returned item.URL has any of the configured
PathPrefixes; reference NewPageLinkDiscovery, Discover and
storage.LinkDiscoveryConfig to locate where to add this assertion.

---

Outside diff comments:
In `@internal/crawler/parser/rule/parser.go`:
- Around line 163-170: The error message returned when both
rule.Selectors.ItemContainer and rule.Selectors.ItemLink are missing (the Error
with Code ErrEmptySelector) incorrectly points users to
LinkDiscovery.ArticleURLPattern even though this branch only runs when
LinkDiscovery is nil; update the Error.Message text to instruct operators to
provide the full link_discovery block (not just ArticleURLPattern) so it
accurately reflects the required configuration when LinkDiscovery is absent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 041050c3-adab-4a1c-b7af-2fb1ad4c76e1

📥 Commits

Reviewing files that changed from the base of the PR and between 0833e0c and 4cfc9a4.

📒 Files selected for processing (5)
  • internal/crawler/parser/rule/discovery.go
  • internal/crawler/parser/rule/parser.go
  • migrations/down/008_relax_link_discovery_patterns.sql
  • migrations/up/008_relax_link_discovery_patterns.sql
  • test/internal/parser/rule/discovery_test.go

Comment thread test/internal/parser/rule/discovery_test.go
Comment thread test/internal/parser/rule/discovery_test.go
@juhy0987 juhy0987 self-assigned this Apr 29, 2026
@juhy0987 juhy0987 added enhancement New feature or request refactor Code refactoring labels Apr 29, 2026
juhy0987 and others added 3 commits April 29, 2026 17:46
- pattern 비어있으면 regex compile 단계 skip → all-pass 모드
- ExcludePatterns + SameOriginOnly + PathPrefixes + MaxLinksPerPage 만으로 필터링
- 매칭 루프에서 pattern==nil 가드 추가
- 0건 매칭 메시지를 모드별로 분기 (pattern stale vs page-empty)

본 시스템 타겟이 article 만이 아닌 페이지 내 모든 의미 있는 글 (이슈 #100 도메인 일반화 의도) 이므로,
사이트별 좁은 article URL regex 강제는 비-article 컨텐츠 (공지/이벤트/시리즈/오피니언) 누락 야기.

본 commit 은 컴포넌트 레벨 변경만 — Parser 분기 + DB 시드 변경은 후속 commits 에서.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
기존: cfg != nil && cfg.ArticleURLPattern != "" → discovery
신규: cfg != nil → discovery (pattern 빈 문자열도 all-pass 모드로 진입)

ItemContainer fallback 은 LinkDiscovery 객체 자체가 nil 일 때만 발동.
운영자가 LinkDiscovery 객체를 두면 명시적으로 discovery 모드 선택한 것으로 간주.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
기존:
- TestPageLinkDiscovery_EmptyPattern_ReturnsEmptySelector — 빈 pattern → ErrEmptySelector expect
- TestParser_ParseLinks_LinkDiscoveryWithEmptyPattern_FallsBackToItemContainer — 빈 pattern → ItemContainer

신규:
- TestPageLinkDiscovery_EmptyPattern_AllPass — 빈 pattern → 모든 same-origin 링크 통과
  + 기본 제외 패턴 (login/mailto 등) 차단 검증
- TestPageLinkDiscovery_EmptyPattern_ExcludeStillApplies — all-pass 모드에서 ExcludePatterns 동작
- TestParser_ParseLinks_LinkDiscoveryWithEmptyPattern_AllPassDiscovery — 빈 pattern + LinkDiscovery 객체 있음 → all-pass discovery (ItemContainer 사용 X)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@juhy0987
juhy0987 merged commit 77422e9 into main Apr 29, 2026
8 checks passed
…ns 강화 (이슈 #148)

migration 007 의 좁은 ArticleURLPattern 을 빈 문자열로 갱신:
- naver / daum / yonhap / cnn 의 list rule 4건 UPDATE
- article_url_pattern: "" → all-pass discovery
- exclude_patterns: 사이트별 노이즈 컷 (광고/공유/네비/미디어/about/login 등)
- same_origin_only / max_links_per_page 는 site profile 유지

운영자가 라이브 모니터링 후 ExcludePatterns 점진 추가하여 noise 정밀화.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@juhy0987
juhy0987 removed the request for review from Copilot April 29, 2026 08:49
juhy0987 and others added 3 commits April 29, 2026 17:55
…m sample (이슈 #148)

기존: 단순 cap (len >= maxOut 시 break) — 페이지 상단 광고/네비 영역에 편향됨

신규 정책:
1. same-origin (raw.URL host 와 동일) 링크는 maxOut 무시하고 모두 통과
2. cross-origin 은 잔여 슬롯 (maxOut - len(same)) 만큼 math/rand/v2.Shuffle 로 무작위 sample
3. maxOut == 0 (무제한) 이면 cross 도 모두 통과

이유:
- 사이트 자체 컨텐츠 (same) 는 noise 적고 가치 높음 — 모두 발행 가치
- 외부 링크 (cross) 는 광고/제휴 noise 많음 — cap 으로 통제 + 무작위 sample 로
  특정 영역 (예: 페이지 상단 sponsored slot) 편향 회피

테스트 재조정 (5건):
- TestPageLinkDiscovery_MaxLinksPerPage_SameOriginUnlimited (same 5건은 cap=2 무시)
- _CrossOriginFillsRemaining (cap 여유 있을 때 cross 채움)
- _Unlimited (maxOut=0 모두 통과)
- _SameOriginOnlyMode (SameOriginOnly=true 면 cross 사전 제거)
- _CrossOriginRandomSample (cross 풀 > 잔여 슬롯 → random 2개 sample)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gemini medium × 2:

- internal/storage/parsing_rule.go LinkDiscoveryConfig 주석 갱신:
  - "빈 ArticleURLPattern → discovery 비활성화" → "all-pass 모드"
  - "ItemContainer fallback 은 LinkDiscovery 자체가 nil 일 때만" 명시
  - MaxLinksPerPage 주석에 same-origin 우선 + cross random sample 정책 명시 (이슈 #148)

- migrations/up/008 주석 갱신:
  - "same_origin_only: true (외부 도메인 차단)" 가 실제 SQL (yonhap만 true) 와 불일치
  - "site profile 유지 (yonhap=true / naver/daum/cnn=false — 기존 설정값 보존)" 으로 정정

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Coderabbit minor × 2:

- TestPageLinkDiscovery_EmptyPattern_PathPrefixesStillApply 신규 케이스
  all-pass 모드에서도 PathPrefixes 가 path 기반 1차 cutoff 로 동작 확인

- TestParser_ParseLinks_LinkDiscoveryWithEmptyPattern_AllPassDiscovery 강화
  기존 listHTML 은 두 경로 (discovery / ItemContainer fallback) 모두 같은 3건 반환 →
  discovery 가 진짜 동작하는지 증명 안 됨.
  ItemContainer 를 매칭 0건 selector 로 변경 — fallback 진입 시 ErrParseFailure 보장,
  그래도 3건 반환되면 discovery 경로 동작의 명백한 증거.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request refactor Code refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] LinkDiscovery 의 ArticleURLPattern 을 optional 로 — 기사만이 아닌 페이지 내 모든 글 수집

1 participant