Skip to content

fix(search): keep Chinese function words out of lexical query admission - #1969

Closed
jasondeng1997 wants to merge 1 commit into
oceanbase:masterfrom
jasondeng1997:fix/cjk-query-function-words
Closed

jasondeng1997 wants to merge 1 commit into
oceanbase:masterfrom
jasondeng1997:fix/cjk-query-function-words

Conversation

@jasondeng1997

@jasondeng1997 jasondeng1997 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1912

Problem

analyze_fts_query is documented to stop unquoted function words from supplying the sole overlap
during a relaxed recall-gate round, but _QUERY_FUNCTION_WORDS is Latin-only, so that filter has
never applied to a Chinese query. Analyzer v1 does not segment Chinese: a run becomes one unigram
per character plus one bigram per adjacent pair. Every function character a Chinese query adds
therefore

  • joins the query's own denominator, required_matches = ceil(len(query_terms) * 0.25), and
  • can also join the numerator when the stored entry happens to contain that character.

Both halves are reproducible for the pair in the report. Against the reported entry text
(; and ? fullwidth here), on ad75ccf2:

query query terms required matched admitted
PowerContext Server 地址 5 2 2 yes
PowerContext Server 地址是什么? 11 3 2 no
的 PowerContext Server 地址是什么? 12 3 3 yes

The third query was admitted because the leading 的 matched the 的 in the entry, crossing a
threshold the query itself had raised. The reported "one extra character makes it match" is
deterministic, not random: the query moves its own admission bar.

Fix

Extend the mechanism that already exists rather than adding a second one.

  • _QUERY_FUNCTION_CHARACTERS mirrors the categories of _QUERY_FUNCTION_WORDS: structural and
    aspectual particles, the copula, coverbs, conjunctions and adverbs, demonstrative and personal
    pronouns, interrogatives and sentence particles, and politeness markers. Negations are absent,
    for the same reason they are absent from the English list. The set also covers the traditional
    writing of the same characters, because Analyzer v1 treats every Han ideograph alike and the
    codepoint is what reaches the filter: 麼 and 么 are different characters, so a
    simplified-only set would leave a traditional query unguarded.
  • _is_query_function_term applies it to Analyzer v1 terms. A unigram is a function word exactly
    when its character is. A bigram is one only when both of its characters are, because a pair
    that carries one content character still carries evidence.
  • Filtering is per occurrence and decided by provenance, not by the shape of the token. A
    u_/b_ term counts as Chinese only when its source span opens on a CJK character, because
    flush_cjk emits those terms over CJK runs while flush_word emits verbatim terms over
    alphanumeric runs. A literal identifier such as u_7684 therefore keeps its documented
    whole-term behavior, while the same string generated from 的 is filtered.
  • Characters that also carry content are deliberately left out of the set — 地 and 得 (地址,
    地方, 得到, 取得) and 有, 会, 能, 要, 帮 (有效, 会议, 功能, 要求, 帮助) — because a lone
    character cannot tell the two uses apart. 著 is left out on the same principle, being also the
    verb in 著名 and 著作.

The same queries after the change:

query query terms required matched admitted
PowerContext Server 地址 5 2 2 yes
PowerContext Server 地址是什么? 6 2 2 yes
的 PowerContext Server 地址是什么? 6 2 2 yes

All six pure-function suffixes covered by the tests (的, 了, 吗, 我的, 是的, 它们的)
leave the term count and the threshold identical to the base query, in both writings.

No threshold, constant or persisted projection changes. analyze_text is untouched, so stored
Analyzer v1 projections keep every term; the filter stays query-side, as the module already
documents.

Scope

  • Not a coverage or threshold change. A longer query whose extra characters carry content
    still raises required_matches exactly as before: 请告诉我 PowerContext Server 地址是什么?
    (11 terms, required 3, matched 2) and 我之前让你记住的 PowerContext Server 地址是什么?
    (14 terms, required 4, matched 2) both remain rejected, as they are on ad75ccf2. Those cases
    are about the 0.25 coverage rule, not about function words.
  • No change to ranking, fusion, channel weights, or the recall-gate verdict order.

The pair rule leaves one boundary, and it is deliberate rather than overlooked. A function
character inserted next to content still contributes one mixed bigram to the denominator, so
的 raises required_matches where the characters around it carry content: alpha 项目地址
keeps 8 terms and needs 2, while alpha 的项目地址 keeps 9 and needs 3. Retiring mixed pairs as
well is exactly what removes the only evidence a two-character compound has — with that rule
行为, 方向 and 并发 each fall to a single term (u_884c, u_65b9, u_5e76) and an exact
phrase query against an entry containing the phrase is refused. Closing the boundary needs the
segmentation Analyzer v1 does not have, so it is documented and pinned by
test_mixed_boundary_bigram_keeps_one_pair_and_pins_the_shift instead of traded for a recall
failure on real compounds.

Tests

tests/builtin/runtime/test_recall_sufficiency.py:

  • test_reported_chinese_function_word_prefix_admits_consistently — the reported pair, on the
    reported entry text.
  • test_chinese_function_words_do_not_raise_the_admission_bar — six pure-function suffixes must
    leave fts_query_requirements unchanged.
  • test_chinese_content_characters_stay_query_evidence — seven content compounds keep their
    unigrams and bigrams.
  • test_chinese_bigram_drops_only_when_both_characters_are_function_words — 址是 is evidence,
    是什/什么 are not.
  • test_content_compound_that_contains_a_listed_character_stays_recallable — 行为, 方向 and
    并发 stay recallable for the exact phrase.
  • test_mixed_boundary_bigram_keeps_one_pair_and_pins_the_shift — the boundary documented above.
  • test_polite_long_query_keeps_the_documented_coverage_rejection — the Scope case stays rejected
    while the shorter question is admitted.
  • test_traditional_chinese_function_characters_share_the_simplified_threshold — sixteen pairs.
  • test_literal_analyzer_shaped_identifier_stays_query_evidence and
    test_literal_identifier_survives_a_query_full_of_chinese_function_words — a literal u_/b_
    identifier keeps whole-term behavior even beside generated function tokens.
  • test_quoted_chinese_function_text_stays_query_evidence — quoting still opts a function word in.
  • test_query_normalization_leaves_the_chinese_projection_unchanged — the persisted projection
    keeps every term.

Validation

  • pytest tests/builtin/runtime/test_recall_sufficiency.py -q — 109 passed
  • pytest tests/builtin/runtime/test_recall_sufficiency.py tests/e2e/test_recall_sufficiency_gate.py -q
    — 146 passed
  • pytest tests/builtin/artifacts/experience/test_search.py tests/builtin/artifacts/topic_memory/test_search.py tests/builtin/persistence/test_topic_memory.py tests/builtin/persistence/test_topic_memory_integrity.py -q
    — 126 passed, 26 skipped
  • ruff format --check, ruff check, ty check on both changed files — clean

Red baseline: with src/powercontext/builtin/artifacts/search.py restored to ad75ccf2, 27
tests fail. With it restored to the previous commit of this branch (which classified a bigram as
functional when either character was), 6 fail — every assertion added for the compounds and
for the mixed-pair boundary above, plus the Scope rejection.

Refs #1912

Copilot AI balanced review requested due to automatic review settings October 13, 2026 06:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Mixed boundary bigrams and analyzer-shaped Latin identifiers still cause incorrect lexical admission.

2 open findings
What changed in this PR

Adds Chinese function-character filtering to stabilize lexical query admission.

Changes:

  • Filters Chinese function unigrams and function-only bigrams.
  • Adds regression and projection-preservation tests.
File Description
src/​powercontext/​builtin/​artifacts/​search.py Adds Chinese query-term classification and filtering.
tests/​builtin/​runtime/​test_recall_sufficiency.py Covers Chinese admission, quoting, and content preservation.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/powercontext/builtin/artifacts/search.py
Comment thread src/powercontext/builtin/artifacts/search.py
@jasondeng1997
jasondeng1997 force-pushed the fix/cjk-query-function-words branch from a8c72c5 to 59530d1 Compare October 13, 2026 07:08
Copilot AI balanced review requested due to automatic review settings October 13, 2026 07:08
@jasondeng1997

Copy link
Copy Markdown
Contributor Author

Rebased onto master (b9edaa5b) and force-pushed, so the head moved a8c72c549 → 59530d16. The only change is the base: this branch is still the same single commit, 2 files, +163 −2.

Why the rebase: the previous head carried a red controlled-cli, which was upstream and not from this branch. That job runs python -m evaluation.zcode_guidance.pin --check and failed with ValueError: skill_pin_mismatch, because #1877 changed the bytes of integrations/zcode/plugins/powercontext/skills/powercontext-project-context without refreshing evaluation/zcode_guidance/skill-lock.json. Upstream tracked it as #1967 and fixed it in #1968, which merged as b9edaa5b (fix(zcode): refresh packaged skill evaluation pin).

b9edaa5b only touches evaluation/zcode_guidance/ (README, skill-lock.json, test_guidance.py), so it has no overlap with the two files here. The pin now verifies locally:

$ .venv/bin/python -m evaluation.zcode_guidance.pin --check
ZCode Skill pin verified: b5bcfe24bcce9c574ee85175d623bb9963970a41
$ echo $?
0

The new head re-ran the full suite and reported 33 checks, none failing. Nothing about the fix itself changed: src/powercontext/builtin/artifacts/search.py and tests/builtin/runtime/test_recall_sufficiency.py are byte-identical to the reviewed revision, and ruff format --check, ruff check and ty check pass on the rebased tree, with tests/builtin/runtime/test_recall_sufficiency.py at 85 passed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Mixed bigrams and unsupported Traditional Chinese forms can still cause function words to change admission thresholds.

3 open findings

🧠 Review effort: Balanced

Comment thread src/powercontext/builtin/artifacts/search.py
@jasondeng1997
jasondeng1997 force-pushed the fix/cjk-query-function-words branch from 59530d1 to 2a0d178 Compare October 13, 2026 08:02
@jasondeng1997

Copy link
Copy Markdown
Contributor Author

Re-triggered: the head moved 59530d16 → 2a0d178d. No content changed — the tree hash is identical (a0101e87), only the commit object was recreated to re-run the workflows. Diff is still the same 2 files, +163 −2.

CI had finished on 59530d16 with 33 success / 1 failure. controlled-cli is now green (the base moved to b9edaa5b, which carries the pin refresh). The one red left was tests (3.12, unit):

FAILED tests/builtin/artifacts/profile/test_scheduler.py::test_startup_scan_catches_up_profile_evidence
  powercontext.builtin.runtime.artifact_processing._WorkerTerminationError:
    Worker exit could not be confirmed
1 failed, 4650 passed, 153 skipped in 188.67s

That is a subprocess-reaping timeout, and it is not from this branch:

  • src/powercontext/builtin/runtime/artifact_processing.py, tests/builtin/artifacts/profile/test_scheduler.py and tests/builtin/runtime/test_artifact_processing.py are byte-identical to master (same blob SHAs);
  • master at b9edaa5b is fully green, and the previous head a8c72c549 — same 2-file diff, base ad75ccf2 — passed all four tests (x, unit) legs;
  • the failing path is ArtifactProcessingSupervisor.close() → _terminate_worker(), which bounds the wait with asyncio.timeout(_WORKER_SHUTDOWN_TIMEOUT_SECONDS) and then raises if the child still has not been reaped. It runs under pytest-xdist ([gw0]), so a loaded runner can exceed that window.

I could not re-run the failed job: I am not a collaborator on this repository, and both POST /actions/runs/{id}/rerun-failed-jobs and POST /actions/jobs/{id}/rerun return 403 Must have admin rights to Repository. Re-creating the commit is the only way I can trigger a re-run from here, which is what this push does. If the leg goes red again on the same test, a maintainer re-run is the right call, and the reap timeout in artifact_processing.py would be worth a separate look.

@jasondeng1997
jasondeng1997 force-pushed the fix/cjk-query-function-words branch from 2a0d178 to c93c678 Compare October 13, 2026 08:37
@jasondeng1997

Copy link
Copy Markdown
Contributor Author

CI finished on 2a0d178d: 32 success, 2 failed, 1 skipped. Both failures are environmental, and the content is unchanged again (c93c678f, same tree a0101e87), so this push is another re-run only.

tests (3.12, unit) — the subprocess-reaping failure reported in my previous comment — passed this time on the identical tree, which settles it as a flake.

controlled-cli

This one is decided by the tree itself: 59530d16 and 2a0d178d have the same tree hash (a0101e87), and controlled-cli went success at 07:08 (job 115174361993) then failure at 08:02 (job 115192990214) with master unmoved at b9edaa5b in between. Same content, opposite outcomes. The failure is also a different one than before — the pin now verifies, and this run died in the host acceptance leg:

FAILED tests/e2e/test_zcode_host_acceptance.py::test_zcode_controlled_host_acceptance[1]
  AssertionError: host_process_exit_3221226505
tests\e2e\zcode_acceptance\runner.py:290
1 failed, 1 passed, 2 deselected in 227.86s

3221226505 is 0xC0000409 (STATUS_STACK_BUFFER_OVERRUN) — the Windows ZCode host process crashing natively while the runner was driving it with --prompt. A Python-side change here cannot produce that, and across the last 20 runs of zcode-acceptance.yml this job is green on every other branch after the pin refresh.

Acceptance (oceanbase)

FAILED tests/e2e/test_atomic_memory_read_snapshot.py::test_atomic_search_keeps_candidates_and_authority_in_one_snapshot[oceanbase-vector]
  sqlalchemy.exc.OperationalError: (pymysql.err.OperationalError) (4184, 'Server out of disk space')
1 failed, 17 passed, 45 deselected in 504.96s

The OceanBase container ran out of disk. That is the runner's storage, not the repository.


Neither red touches src/powercontext/builtin/artifacts/search.py or tests/builtin/runtime/test_recall_sufficiency.py. Since I cannot re-run jobs from a fork, re-creating the commit is the only way I can roll again; the result of this third attempt will follow.

@jasondeng1997
jasondeng1997 force-pushed the fix/cjk-query-function-words branch from c93c678 to 902f5bf Compare October 13, 2026 09:00
@jasondeng1997

Copy link
Copy Markdown
Contributor Author

Rolled again (c93c678f → 902f5bf8, same tree a0101e87). Before you read the runs, here is the evidence that settles the CI question.

The same tree has now been through CI three times

59530d16, 2a0d178d and c93c678f are three commit objects with a single identical tree (a0101e87), all based on b9edaa5b, which has not moved. Their outcomes:

job 59530d16 2a0d178d c93c678f
controlled-cli pass fail fail
tests (3.12, unit) fail pass pass
tests (3.14, e2e) pass pass fail
Acceptance (oceanbase) pass fail pass
tests (3.11/3.13, unit) pass pass pass
tests (3.11/3.12/3.13, e2e) pass pass pass

Every job that has ever failed on this branch has also passed on this exact tree. No job fails deterministically for this content, so there is no commit-level fix that could make them pass — re-running is the only thing that changes the result.

What each red actually is

  • controlled-cli, 2a0d178d: AssertionError: host_process_exit_3221226505 — 3221226505 is 0xC0000409 (STATUS_STACK_BUFFER_OVERRUN), a native crash of the Windows ZCode host process (tests/e2e/test_zcode_host_acceptance.py, runner.py:290 asserts returncode == 0).
  • controlled-cli, c93c678f: a different failure, AssertionError: concurrent_host_failed (tests/e2e/zcode_acceptance/lifecycle.py:105), parameter [2] instead of [1]. The captured stderr shows the hosts racing on the same artifact (412 Precondition Failed, Artifact ETag does not match the current head, provided_etag "revision:1" vs current_etag "revision:2").
  • tests (3.14, e2e), c93c678f: tests/e2e/test_statistics_flow.py::test_statistics_survive_the_authenticated_http_business_flow_and_restart[sqlite] asserts statistics.usage.totals.generation.requests == 2 and got 1, with WARNING powercontext.builtin.runtime._model_usage: Model usage write failed; record will not be retried immediately above it — one usage record was dropped by the write path. I ran this test three times locally on this tree: 3 passed.
  • tests (3.12, unit), 59530d16: test_startup_scan_catches_up_profile_evidence → _WorkerTerminationError: Worker exit could not be confirmed from ArtifactProcessingSupervisor.close(), which bounds _terminate_worker() with asyncio.timeout(_WORKER_SHUTDOWN_TIMEOUT_SECONDS).
  • Acceptance (oceanbase), 2a0d178d: pymysql.err.OperationalError: (4184, 'Server out of disk space') — the container ran out of disk.

Scope

None of these paths is reachable from src/powercontext/builtin/artifacts/search.py, whose only change is which unquoted function words count as lexical evidence when building an FTS query, or from tests/builtin/runtime/test_recall_sufficiency.py. artifact_processing.py, _model_usage.py and the ZCode host acceptance suite are untouched and byte-identical to master.

Since I have no write access here and both rerun-failed-jobs and jobs/{id}/rerun return 403 Must have admin rights to Repository, re-creating the commit is the only way I can roll again; this push does that. The PR itself is ready — it needs a maintainer re-run, not a code change.

@jasondeng1997
jasondeng1997 force-pushed the fix/cjk-query-function-words branch from 902f5bf to d913ab5 Compare October 13, 2026 09:27
Copilot AI balanced review requested due to automatic review settings October 13, 2026 09:27
@jasondeng1997

Copy link
Copy Markdown
Contributor Author

Rebased onto current master, plus evidence that the two remaining reds are not from this change

Rebased fix/cjk-query-function-words onto ee50b36d (current master, which now includes #1783 and #1964). The patch is byte-identical before and after the rebase — still exactly two files:

src/powercontext/builtin/artifacts/search.py      | 94 ++++++++++++++++-
tests/builtin/runtime/test_recall_sufficiency.py  | 71 ++++++++++++++

Verified locally on the new base, running the same commands CI runs:

  • .venv/bin/python -m evaluation.zcode_guidance.pin --check → ZCode Skill pin verified: b5bcfe24bcce9c574ee85175d623bb9963970a41 (exit 0)
  • pytest tests/builtin/runtime/test_recall_sufficiency.py → 85 passed

controlled-cli — repo-wide flake, not this diff

The failure is host_process_exit_3221226505 (0xC0000409) — a Windows native crash in the ZCode host process, plus 412 Precondition Failed / 409 Conflict revision races on the artifact path.

This reproduces on an unrelated PR right now. PR #1933 (42 files, artifact reversion handles — nothing to do with lexical query admission) fails this job with the identical signature, in the same CI window:

AssertionError: host_process_exit_3221226505
FAILED tests/e2e/test_zcode_host_acceptance.py::test_zcode_controlled_host_acceptance[1]

On this branch, the same job passed on the same tree in an earlier run, and the ZCode CLI build is byte-identical between the passing and the failing run — same lockfile (upstream is up to date), same 1739 packages, same dist\zcode.cjs 29.7mb / .map 50.1mb, same ZCode Skill pin verified: b5bcfe24.... The only difference is where the acceptance lifecycle lands:

HTTP 412 Precondition Failed
  'code': 'revision_conflict', 'message': 'Artifact ETag does not match the current head.'
  'details': {'provided_etag': '"revision:1"', 'current_etag': '"revision:2"'}
HTTP 409 Conflict
  'code': 'candidate_conflict', 'message': 'The Candidate version is stale.'

That is a race inside the host acceptance lifecycle, not something this diff can reach.

tests (3.12, unit) — also flaky on master itself

This job fails on master's own commit 8970a970c (#1964), on a different test:

FAILED tests/test_server.py::test_server_factory_reports_missing_embedding_api_prefix_as_degraded

On this branch it failed once with test_family_processing.py::test_worker_records_existing_model_usage_purposes_and_replay_does_not_infer[experience] (sqlalchemy.exc.NoResultFound plus RuntimeError: Event loop is closed) and passed in the other runs. Both tests live outside this change, and neither imports powercontext.builtin.artifacts.search.

Run history on one unchanged tree

This branch went through four runs on a single tree a0101e87 before the rebase. Every job that failed in one run passed in another on that same tree:

job run 1 59530d16 run 2 2a0d178d run 3 c93c678f run 4 902f5bf8
controlled-cli pass fail fail fail
tests (3.12, unit) fail pass pass fail
tests (3.14, e2e) pass pass fail —
Acceptance (oceanbase) pass fail pass —
remaining 5 test legs pass pass pass pass

Net

The change itself is complete and verified. What is left is CI infra flakiness that also fires on master and on other contributors' PRs — controlled-cli needs a re-run here, not another commit. If it would help, I am happy to open a separate issue tracking the 0xC0000409 host crash and the artifact-ETag race.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Mixed CJK bigrams can still raise admission thresholds, and the previously confirmed encoded-identifier regression remains unresolved.

3 open findings

🧠 Review effort: Balanced

@AlexStocks

Copy link
Copy Markdown
Contributor

@jasondeng1997 I have approved your PR. please fix the ci failed. and reply the copilot's review comment.

Copilot AI balanced review requested due to automatic review settings October 14, 2026 00:52
@jasondeng1997
jasondeng1997 force-pushed the fix/cjk-query-function-words branch from d913ab5 to 207a63d Compare October 14, 2026 00:52
@jasondeng1997

Copy link
Copy Markdown
Contributor Author

@AlexStocks thanks for the approval. Both requests are done.

Review comments — all four answered in place, one per thread. They were substantive and two were real bugs in this PR, not style:

  1. Literal analyzer-shaped identifiers (4265652772) — a literal u_7684 was being decoded as the character 的 and withdrawn from admission. Confirmed by @hidb4ai with real SQLite Memory search: stored alpha u_7684 queried with alpha beta gamma u_7684 returned nothing, where the base filter returns the entry.
  2. Mixed boundary bigrams (4265653445) — alpha 的项目地址 kept b_7684_9879, so inserting 的 still raised the requirement from 2 to 3 even after 的 itself was filtered.
  3. Traditional forms (4265653780) — 麼 誰 嗎 們 這 還 請 對 給 讓 從 於 與 為 並 過 were unguarded, because Analyzer v1 sees codepoints and the set only had simplified spellings.

What 207a63d3 changes

Filtering is now per occurrence and decided by provenance rather than by the shape of the token. analyze_text_with_spans already records where each term came from — flush_cjk emits u_/b_ terms over CJK runs, flush_word emits verbatim terms over alphanumeric runs — so a u_/b_ term counts as Chinese only when its source span opens on a CJK character. A bigram counts as a function word when either character is one. _QUERY_FUNCTION_WORDS is back to the whole-term rule alone. analyze_text is still untouched, so persisted projections and every threshold constant are unchanged.

Measured (same probes the reviewers used):

case d913ab5a 207a63d3
地址是什麼 / 地址是什么 6 / 4 terms 3 / 3
alpha 项目地址 / alpha 的项目地址 8 req 2 / 9 req 3 8 req 2 / 8 req 2
alpha u_7684 vs alpha beta gamma u_7684 overlap 1, rejected overlap 2, admitted

Local verification before pushing: pytest tests/builtin/runtime/test_recall_sufficiency.py tests/e2e/test_recall_sufficiency_gate.py -q → 142 passed; the four related suites → 126 passed, 26 skipped; ruff format --check, ruff check, ty check clean; evaluation.zcode_guidance.pin --check → ZCode Skill pin verified: b5bcfe24....

Red-tested: with search.py restored to d913ab5a, 20 of the 21 targeted new tests fail; with it restored to ad75ccf2, 19 fail. The new tests capture the reported behaviour rather than restating it.

On CI — the previous head d913ab5a finished fully green (34 success, 1 skipped, 0 failed), so the reds you saw were the flaky jobs, not this change: controlled-cli (host_process_exit_3221226505, and PR #1933 failed it with the identical signature in the same window) and tests (3.12 unit) (which also fails on master's own commit 8970a970c, on a different test). This push re-runs everything on the new head.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Character-wide filtering misclassifies common content compounds and can reject exact lexical matches.

1 open finding
3 resolved since last review

🧠 Review effort: Balanced

Comment thread src/powercontext/builtin/artifacts/search.py
@AlexStocks AlexStocks added the 1.4.0 v1.4.0 release tracking label Oct 14, 2026
@jasondeng1997
jasondeng1997 force-pushed the fix/cjk-query-function-words branch from 207a63d to 2271a86 Compare October 15, 2026 02:14
Copilot AI balanced review requested due to automatic review settings October 15, 2026 02:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The confirmed content-compound recall regression remains unresolved, and implemented admission behavior contradicts the stated scope.

2 open findings

🧠 Review effort: Balanced

Comment thread src/powercontext/builtin/artifacts/search.py Outdated
analyze_fts_query documents that unquoted function words cannot supply the sole overlap
during a relaxed recall-gate round, but _QUERY_FUNCTION_WORDS is Latin-only, so that filter
never applied to a Chinese query. Analyzer v1 does not segment Chinese: a run becomes one
unigram per character plus one bigram per adjacent pair. Every function character a Chinese
query adds joins the query own denominator, required_matches = ceil(len(query_terms) * 0.25),
and can also join the numerator when the stored entry happens to contain that character.

Against the entry text reported in the issue, on ad75ccf, "PowerContext Server 地址是什么?"
has 11 terms, required 3 and matched 2, so it is rejected, while "的 PowerContext Server
地址是什么?" has 12 terms, required 3 and matched 3, so it is admitted. The leading 的
matched the 的 in the entry and crossed a threshold the query itself had raised, which is why
one extra character flipped the result.

_QUERY_FUNCTION_CHARACTERS mirrors the categories of _QUERY_FUNCTION_WORDS and also covers
the traditional writing of the same characters, because Analyzer v1 treats every Han
ideograph alike and codepoints are what reach the filter: 麼 and 么 are different characters,
so a simplified-only set would leave a traditional query unguarded. Negations are absent for
the same reason they are absent in the English list. Characters that also carry content stay
out, because a lone character cannot distinguish 地址, 地方, 得到 and 取得 from the particle
地, and 有, 会, 能, 要 and 帮 appear in 有效, 会议, 功能, 要求 and 帮助; 著 is left out on
the same principle, being also the verb in 著名 and 著作.

Filtering is per occurrence and decided by provenance rather than by the shape of the token.
A u_/b_ term counts as Chinese only when its source span opens on a CJK character, because
flush_cjk emits those terms over CJK runs while flush_word emits verbatim terms over
alphanumeric runs. A literal identifier such as u_7684 therefore keeps its documented
whole-term behavior, while the same string generated from 的 is filtered.

A bigram counts as a function word only when both of its characters are one. Several listed
characters also carry content (为 in 行为, 向 in 方向, 并 in 并发), and a bigram is the only
evidence a two-character compound has, so retiring every pair that merely touches a listed
character removes the evidence rather than the function word. Measured with real SQLite
Memory search, 行为, 方向 and 并发 each lost their pair and stopped being returned for the
exact phrase; under this rule all three are returned again.

The change is query-side only. analyze_text is untouched, so persisted Analyzer v1
projections keep every term, and no threshold, constant or fusion input changes. The short
question from the report is admitted, the long form this pull request documents as rejected
stays rejected, and appending a pure function word to a query leaves the threshold unchanged
in both writings.

The boundary the pair rule leaves is deliberate. An inserted function character beside
content can still contribute one mixed bigram, so 的 raises required_matches where the
characters around it carry content: "alpha 项目地址" keeps 8 terms and needs 2, while
"alpha 的项目地址" keeps 9 and needs 3. Closing that would need the segmentation Analyzer v1
does not have, and the attempt is what cost the three compounds above.

Tested: pytest tests/builtin/runtime/test_recall_sufficiency.py -q (109 passed) and together
with tests/e2e/test_recall_sufficiency_gate.py (146 passed); pytest
tests/builtin/artifacts/experience/test_search.py
tests/builtin/artifacts/topic_memory/test_search.py
tests/builtin/persistence/test_topic_memory.py
tests/builtin/persistence/test_topic_memory_integrity.py -q (126 passed, 26 skipped); ruff
format --check, ruff check and ty check on both changed files. With search.py restored to
ad75ccf, 27 of these tests fail; with it restored to the previous commit of this branch, 6
fail, including every assertion added for the compounds above.

Refs oceanbase#1912
Copilot AI balanced review requested due to automatic review settings October 15, 2026 05:32
@jasondeng1997
jasondeng1997 force-pushed the fix/cjk-query-function-words branch from 2271a86 to 317d072 Compare October 15, 2026 05:32
@jasondeng1997

Copy link
Copy Markdown
Contributor Author

@AlexStocks Both of your requests are addressed, and this revision also fixes a regression my own previous commit introduced.

CI — the previous head went fully green (36 success / 1 skipped / 0 failed) once rebased onto master. The reds before that were repository-wide flakes rather than this change: another contributor's PR #1933 (a feature branch with no overlap) failed controlled-cli with the byte-identical host_process_exit_3221226505, and Acceptance (oceanbase) fails on (4184, 'Server out of disk space'). This push is rebased onto 7f38426e, which now carries #1998 (stabilize pending boundary test timing) and the merged #1933.

Copilot's comments — the original four threads were answered on the previous revision; the two still unresolved are answered now:

  • search.py:104 (confirmed against real SQLite Memory search by @hidb4ai): retiring every bigram that touches a function character also removed the only evidence 行为, 方向 and 并发 have, so an exact-phrase query stopped being returned. The pair rule is back to both characters, all three are recallable again, and test_content_compound_that_contains_a_listed_character_stays_recallable pins it.
  • search.py:410: the ## Scope section promises 请告诉我 PowerContext Server 地址是什么? stays rejected, and the widened rule had broken that promise. It is rejected again (11 terms, needs 3, matches 2), pinned by test_polite_long_query_keeps_the_documented_coverage_rejection.

One boundary remains, and it is now documented rather than claimed away: a function character inserted next to content still contributes one mixed bigram, so alpha 项目地址 keeps 8 terms and needs 2 while alpha 的项目地址 keeps 9 and needs 3. Closing it needs the segmentation Analyzer v1 does not have, and the attempt is exactly what cost the three compounds above.

Verification on 317d0721: 109 passed in the recall-sufficiency file, 146 with the e2e gate, 126 passed / 26 skipped across the related suites, ruff and ty clean. With search.py restored to the previous commit of this branch, 6 of the new assertions fail; restored to ad75ccf2, 27 fail.

The two review threads are answered but still show as unresolved — that toggle needs a maintainer.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Content-unigram removal admits unrelated short-query matches and rejects previously admitted compound matches in longer queries.

1 open finding
2 resolved since last review

🧠 Review effort: Balanced

Comment on lines +410 to +411
if term.startswith("u_"):
return _analyzed_character(term[2:]) in _QUERY_FUNCTION_CHARACTERS
@hidb4ai

hidb4ai commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Let's proceed with #1979 as the main implementation for word analysis and query filtering, and close #1969 for now to avoid maintaining overlapping approaches.

Please carry the regression cases from this PR into #1979's validation, including the reported Chinese query variants, literal analyzer-shaped identifiers, and content compounds such as 行为, 方向 and 并发. Closing this PR does not mean those cases are already verified in #1979.

@hidb4ai hidb4ai closed this Oct 15, 2026
@hidb4ai hidb4ai removed the 1.4.0 v1.4.0 release tracking label Oct 15, 2026
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.

bug(recall): identical fact is returned for one query form and missed for another — retrieval flips on 1-2 character prefixes (CJK)

4 participants