# NousResearch/hermes-agent · Short CJK OR queries in session search returning no hits

## Open now

- Nothing open on GitHub.

The members concern session_search returning zero results for OR-combined CJK queries of two characters or fewer, where the trigram approach is selected but cannot match. Two pull requests propose routing such queries to a LIKE fallback, one described as a salvage of #22649.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #22847. Verify it addresses your case.
- **Recommended:** read PR #22847 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-22847`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #20494: session_search returns 0 hits for OR-combined ≤2-char CJK queries (trigram picked but can't match)

## Prior & competing PRs

- **#22847: fix(session): route OR-combined short CJK tokens to LIKE fallback (salvage #22649)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_state.py`, `tests/test_hermes_state.py`
  - fix hunks:
  - `hermes_state.py`

    ```diff
    @@ -1958,7 +1958,19 @@ def search_messages(
                 raw_query = query.strip('"').strip()
                 cjk_count = self._count_cjk(raw_query)
     
    -            if cjk_count >= 3:
    +            # Per-token CJK length check (#20494): trigram needs >=3 CJK chars
    +            # per token. A query like "广西 OR 桂林 OR 漓江" has cjk_count=6
    +            # (>=3) but each individual token is only 2 chars — trigram returns 0.
    +            # Route to LIKE when any non-operator CJK token is <3 CJK chars.
    +            _tokens_for_check = [
    +                t for t in raw_query.split()
    +                if t.upper() not in ("AND", "OR", "NOT") and self._contains_cjk(t)
    +            ]
    +            _any_short_cjk = any(
    +                self._count_cjk(t) < 3 for t in _tokens_for_check
    +            )
    +
    +            if cjk_count >= 3 and not _any_short_cjk:
                     # Trigram FTS5 path — quote each non-operator token to handle
                     # FTS5 special chars (%, *, etc.) while preserving boolean
                     # operators (AND, OR, NOT) for multi-term queries.
    ```

    ```diff
    @@ -2009,11 +2021,24 @@ def search_messages(
                         else:
                             matches = [dict(row) for row in tri_cursor.fetchall()]
                 else:
    -                # Short CJK query (1-2 chars) — trigram needs ≥3 CJK chars.
    -                # Fall back to LIKE substring search.
    -                escaped = raw_query.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
    -                like_where = ["(m.content LIKE ? ESCAPE '\\' OR m.tool_name LIKE ? ESCAPE '\\' OR m.tool_calls LIKE ? ESCAPE '\\')"]
    -                like_params: list = [f"%{escaped}%", f"%{escaped}%", f"%{escaped}%"]
    +                # Short / mixed CJK query: trigram cannot match tokens with
    +                # <3 CJK chars. Fall back to LIKE substring search.
    +                # For multi-token OR queries (e.g. "广西 OR 桂林 OR 漓江"),
    +                # build one LIKE condition per non-operator token so each term
    +                # is matched independently (#20494).
    +                non_op_tokens = [
    +                    t for t in raw_query.split()
    +                    if t.upper() not in ("AND", "OR", "NOT")
    +                ] or [raw_query]
    +                token_clauses = []
    +                like_params: list = []
    +                for tok in non_op_tokens:
    +                    esc = tok.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
    +                    token_clauses.append(
    +                        "(m.content LIKE ? ESCAPE '\\' OR m.tool_name LIKE ? ESCAPE '\\' OR m.tool_calls LIKE ? ESCAPE '\\')"
    +                    )
    +                    like_params += [f"%{esc}%", f"%{esc}%", f"%{esc}%"]
    +                like_where = [f"({' OR '.join(token_clauses)})"]
                     if source_filter is not None:
                         like_where.append(f"s.source IN ({','.join('?' for _ in source_filter)})")
                         like_params.extend(source_filter)
    ```

    ```diff
    @@ -2037,8 +2062,8 @@ def search_messages(
                         LIMIT ? OFFSET ?
                     """
                     like_params.extend([limit, offset])
    -                # instr() parameter goes first in the bound list
    -                like_params = [raw_query] + like_params
    +                # instr() for snippet uses first search token
    +                like_params = [non_op_tokens[0]] + like_params
                     with self._lock:
                         like_cursor = self._conn.execute(like_sql, like_params)
                         matches = [dict(row) for row in like_cursor.fetchall()]
    ```
  - `tests/test_hermes_state.py`

    ```diff
    @@ -957,6 +957,39 @@ def test_cjk_trigram_preserves_boolean_operators(self, db):
             session_ids = {r["session_id"] for r in results}
             assert session_ids == {"s1", "s2"}
     
    +    def test_cjk_or_combined_short_tokens_returns_results(self, db):
    +        """Regression test for #20494.
    +
    +        OR-combined 2-char CJK tokens (e.g. "广西 OR 桂林 OR 漓江 OR 旅游")
    +        previously returned 0 results because _count_cjk of the whole query
    +        was >=3 (8 chars here), selecting the trigram path, but each individual
    +        token is only 2 CJK chars and trigram requires >=3 chars per token.
    +        The per-token check must route such queries to the LIKE fallback.
    +        """
    +        db.create_session(session_id="s1", source="cli")
    +        db.create_session(session_id="s2", source="telegram")
    +        db.create_session(session_id="s3", source="cli")
    +        db.append_message("s1", role="user", content="广西是个好地方，去过桂林")
    +        db.append_message("s2", role="user", content="漓江风景很美，值得旅游")
    +        db.append_message("s3", role="user", content="unrelated English content")
    +
    +        results = db.search_messages("广西 OR 桂林 OR 漓江 OR 旅游")
    +        session_ids = {r["session_id"] for r in results}
    +        assert "s1" in session_ids, "广西/桂林 terms not matched"
    +        assert "s2" in session_ids, "漓江/旅游 terms not matched"
    +        assert "s3" not in session_ids, "unrelated message must not match"
    +
    +    def test_cjk_short_token_or_query_preserves_filters(self, db):
    +        """Source filter applies correctly in the short-token LIKE path (#20494)."""
    +        db.create_session(session_id="s1", source="cli")
    +        db.create_session(session_id="s2", source="telegram")
    +        db.append_message("s1", role="user", content="广西旅游攻略cli")
    +        db.append_message("s2", role="user", content="广西旅游攻略telegram")
    +
    +        results = db.search_messages("广西 OR 旅游", source_filter=["telegram"])
    +        assert len(results) == 1
    +        assert results[0]["source"] == "telegram"
    +
     
     # =========================================================================
     # Session search and listing
    ```
- **#20499: fix(session-search): route short CJK OR queries to LIKE**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `058c50816c70c5f9a1253a87776d50e8df4c5dcf`, `#20494`

## Fix status

- Merged to main in PR #22847 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-10`.
- Subsystem: `agent`
