Skip to content

createSearcher 세션 최적화가 chosung:false 멀티필드에서 unsound (순방향 타이핑 시 항목 실종) #35

Description

@mundi4

TL;DR

createSearcher 멀티필드 모드에서 chosung: false 필드가 하나라도 있으면 세션 최적화(이전 매치만 재스캔)가 unsound하다. IME 순방향 타이핑처럼 한 searcher에 쿼리를 점점 늘려 호출하면, 제목/노트 등 chosung:false 필드로만 매치되는 항목이 실종된다. fresh searcher로는 찾아진다. createSearcher 없이 재현되지 않으므로 세션 로직 버그다.

재현 (test/에 drop-in, casely 무관)

import { describe, expect, it } from "vitest";
import { createSearcher } from "../src/index";

describe("session reuse unsound with per-field chosung:false", () => {
  const items = [
    { id: 1, title: "판결이 동일한", name: "아무개" }, // "판/판결"엔 title(chosung:false)로만 관련
    { id: 2, title: "무관한 제목", name: "표범수" },   // name이 ㅍ으로 시작
  ];
  const opts = {
    fields: [
      { key: (i: (typeof items)[number]) => i.title, chosung: false },
      { key: (i: (typeof items)[number]) => i.name, chosung: true },
    ],
    whitespace: "split" as const,
  };

  it("forward typing drops a title-only match that a fresh search finds", () => {
    const s = createSearcher(items, opts);
    const forward = ["ㅍ", "파", "판", "판결"].map((q) => s.search(q).map((r) => r.item.id));
    const fresh = createSearcher(items, opts).search("판결").map((r) => r.item.id);

    expect(fresh).toEqual([1]);          // fresh는 찾음
    expect(forward.at(-1)).toContain(1); // FAILS: 세션이 잃어버림
  });
});

관측:

forward: [ ㅍ:[2], 파:[], 판:[], 판결:[] ]
fresh 판결: [1]

근본 원인

두 기능의 상호작용:

  1. 세션 재사용 판정src/createSearcher.ts, makeRuntime 내부(대략 line 147-155):

    // 이전 모든 토큰이 각각 어떤 현재 토큰의 atom-prefix 이면 매치 집합은 단조 축소 →
    const canReuse =
      prevTokens.length > 0 &&
      prevTokens.every((p) => p.length > 0 && currentTokens.some((c) => c.startsWith(p))) &&
      flagsCompatible &&
      prevMatchedIndices !== null;

    "atom-prefix 확장 = 매치 집합 단조 축소"를 가정하고 prevMatchedIndices만 재스캔한다.

  2. chosung 하드 필터src/matchFields.ts:66:

    if (chosung && fields[i].chosung === false) continue; // 초성-only 토큰은 chosung:false 필드 스킵

충돌: 초성-only 토큰 "ㅍ"(atoms=자음 하나)은 "파"(atoms=자음+모음)의 atom-prefix다 → canReuse=true. 그러나 "ㅍ"은 초성-only라 title(chosung:false)에서 제외됐고, "파"는 초성-only가 아니라 제외되지 않는다. 따라서 title로만 매치되는 항목(id1)은 "ㅍ" 매치엔 없다가 "파" 매치엔 생긴다 → 매치 집합이 커진다(단조 축소 위반). prevMatchedIndices(id1 없음)만 재스캔하니 id1은 세션이 리셋될 때까지(쿼리가 짧아져 prefix 규칙이 깨질 때까지) 영구 실종.

"토큰의 초성-only 여부"는 atom-prefix에 대해 단조가 아니다 (모음 atom 하나가 붙으면 초성-only=true→false로 뒤집혀 chosung:false 필드가 un-gate됨). 세션 판정은 이 전이를 무효화하지 않는다.

chosung:false 필드가 없으면(모두 chosung:true 또는 단일 필드) 단조성이 성립해 문제없다 — 그래서 이름 필드로 매치되는 항목은 멀쩡히 나온다.

수정안

세션 재사용 가드를 chosung-aware로. chosung:false 필드가 있는 멀티필드 searcher에서만 적용.

  • 정밀안(권장): 이전 쿼리의 토큰별 "초성-only" 플래그를 prevTokens와 함께 저장. 재사용 조건에 추가: prefix로 대응되는 (prev p → current c) 쌍 중 p가 초성-only인데 c가 초성-only가 아니면 재사용 불가(full scan). (isChosungOnlyToken은 현재 matchFields.ts 내부 함수 → internal/로 옮겨 공유하거나 export.)
  • 보수안(간단): hasChosungFalseField이고 이전 쿼리에 초성-only 토큰이 하나라도 있었으면 재사용 금지. 초성-only 토큰은 짧고 초기에만 나오므로 perf 손실 미미.

구현 위치: createSearcher의 멀티필드 빌더가 fields.some(f => f.chosung === false)를 계산해 makeRuntime에 전달(새 파라미터/재사용-predicate 훅), makeRuntimecanReuse에 위 조건을 AND.

검증

  • 위 재현 테스트가 통과해야 함.
  • test/에 회귀 테스트로 추가(순방향 타이핑 시퀀스 + fresh 비교).
  • 기존 단조 케이스(모두 chosung:true, 단일 필드)의 세션 재사용은 그대로 유지되는지 확인.
  • npm test + npm run check:fix.

영향 범위

  • 영향: chosung:false 필드를 가진 멀티필드 createSearcher를 한 인스턴스로 순방향 타이핑에 쓰는 소비자(= command palette / 실시간 검색 전형).
  • 비영향: 단일 필드, 모든 필드 chosung:true, fresh searcher/matchFields 직접 호출.

참고 (문서)

CLAUDE.md는 "타이핑할수록 결과가 단조감소(monotonic narrowing)"를 핵심 속성으로 명시하지만 chosung:false 하드 필터가 이를 깬다. 세션을 고치거나, 최소한 이 상호작용을 문서화해야 한다.

소비자측 현황 (casely)

casely는 이 세션 버그를 피하려고 createSearcher 대신 matchFields를 아이템별로 직접 루프한다(세션 최적화 포기). fuzzly에서 세션이 고쳐지면 casely도 createSearcher로 되돌려 세션 이득을 취할 수 있다.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions