배경
SearcherOptions.scoring이 함수 형태((target) => ScoringConfig)일 때 현재 구현은 매 search 호출, 매 평가 엔트리마다 콜백을 재호출한다:
- 단일 필드:
createSearcher.ts evaluate 내 resolveScoringConfig(t) (~L311)
- 멀티필드: 매 evaluate마다
matchFields(query, fieldBuf, { scoring: scoringOpt, strict }) (~L376) → matchFields 내부에서 fieldCfgs 재계산 (~L56)
소비자가 createGraphemeBonuses 같은 per-target 비용이 있는 config를 쓰면 키스트로크마다 전 아이템 재계산이 일어난다. 시그니처가 이미 "target만의 함수"를 선언하고 있고 entry의 target은 생성 후 불변이므로, entry 생성 시 1회 resolve해 캐시하는 것이 동작 변화 없는 안전한 수정이다.
대안으로 검토한 "Target에 bonus를 굽기"는 PREPROCESS_VERSION/IDB 직렬화 계약을 건드리므로 기각.
계약 변경 (문서화 필수)
scoring 함수는 이제 searcher 생성 / add() / replaceAll() 시 entry당 1회 호출된다 (매 검색 아님). target만의 순수함수여야 한다. matchBest/matchFields를 직접 호출하는 low-level 경로는 기존대로 호출 시점 resolve (변경 없음 — 캐시는 searcher 계층의 책임).
변경 사항
src/types.ts
MatchField에 scoring?: ScoringConfig 추가 — pre-resolved per-field config, matchFields의 opts.scoring보다 우선. JSDoc에 우선순위 명시
SearcherOptions.scoring / MultiFieldSearcherOptions.scoring JSDoc 갱신: "함수 형태면 entry 생성 시(searcher 생성/add/replaceAll) 1회 평가되어 캐시된다. target만의 순수함수여야 한다." (기존 "matchBest 호출 직전에 평가" 문구 교체)
src/matchFields.ts
fieldCfgs 계산(~L56)을 fields.map((f) => f.scoring ?? cfgFor(f.target))로 변경
src/createSearcher.ts
- 단일 필드 (
createSingleFieldSearcher): entry를 { item, target, scoring?: ScoringConfig }로 확장, toEntry에서 resolveScoringConfig?.(target) resolve. evaluate는 entry.scoring을 matchBest에 그대로 전달 (resolver 호출 제거)
- 멀티필드 (
createMultiFieldSearcher):
scoringOpt이 함수면 toEntry에서 scorings: targets.map((t) => scoringOpt(t)) 캐시. 객체면 entry 저장 없이 공유 상수 staticCfg 하나
- evaluate의 fieldBuf 채우기 루프에서
fieldBuf[f].scoring = entry.scorings ? entry.scorings[f] : staticCfg 세팅
matchFields 호출에서 scoring opts 제거 → { strict }만 전달
테스트 (신규)
vi.fn으로 scoring 함수 호출 횟수 검증:
- 단일 필드: N개 items로 생성 → 정확히 N회.
search() 3연속 호출 → 증가 없음. add(item) → +1. replaceAll(M개) → 총 +M
- 멀티필드: N items × F fields → N×F회. search 후 증가 없음
- 스코어 값 자체의 불변성은 기존 스코어 테스트 green으로 담보 (resolve 시점만 이동, 값 동일)
완료 기준
배경
SearcherOptions.scoring이 함수 형태((target) => ScoringConfig)일 때 현재 구현은 매 search 호출, 매 평가 엔트리마다 콜백을 재호출한다:createSearcher.tsevaluate 내resolveScoringConfig(t)(~L311)matchFields(query, fieldBuf, { scoring: scoringOpt, strict })(~L376) →matchFields내부에서fieldCfgs재계산 (~L56)소비자가
createGraphemeBonuses같은 per-target 비용이 있는 config를 쓰면 키스트로크마다 전 아이템 재계산이 일어난다. 시그니처가 이미 "target만의 함수"를 선언하고 있고 entry의 target은 생성 후 불변이므로, entry 생성 시 1회 resolve해 캐시하는 것이 동작 변화 없는 안전한 수정이다.대안으로 검토한 "Target에 bonus를 굽기"는
PREPROCESS_VERSION/IDB 직렬화 계약을 건드리므로 기각.계약 변경 (문서화 필수)
scoring 함수는 이제 searcher 생성 /
add()/replaceAll()시 entry당 1회 호출된다 (매 검색 아님). target만의 순수함수여야 한다.matchBest/matchFields를 직접 호출하는 low-level 경로는 기존대로 호출 시점 resolve (변경 없음 — 캐시는 searcher 계층의 책임).변경 사항
src/types.tsMatchField에scoring?: ScoringConfig추가 — pre-resolved per-field config,matchFields의opts.scoring보다 우선. JSDoc에 우선순위 명시SearcherOptions.scoring/MultiFieldSearcherOptions.scoringJSDoc 갱신: "함수 형태면 entry 생성 시(searcher 생성/add/replaceAll) 1회 평가되어 캐시된다. target만의 순수함수여야 한다." (기존 "matchBest 호출 직전에 평가" 문구 교체)src/matchFields.tsfieldCfgs계산(~L56)을fields.map((f) => f.scoring ?? cfgFor(f.target))로 변경src/createSearcher.tscreateSingleFieldSearcher): entry를{ item, target, scoring?: ScoringConfig }로 확장, toEntry에서resolveScoringConfig?.(target)resolve. evaluate는entry.scoring을matchBest에 그대로 전달 (resolver 호출 제거)createMultiFieldSearcher):scoringOpt이 함수면 toEntry에서scorings: targets.map((t) => scoringOpt(t))캐시. 객체면 entry 저장 없이 공유 상수staticCfg하나fieldBuf[f].scoring = entry.scorings ? entry.scorings[f] : staticCfg세팅matchFields호출에서scoringopts 제거 →{ strict }만 전달테스트 (신규)
vi.fn으로 scoring 함수 호출 횟수 검증:search()3연속 호출 → 증가 없음.add(item)→ +1.replaceAll(M개)→ 총 +M완료 기준
npm run check:fixclean