fix(cli): multi-word and literal-wildcard search in odysseus-docs (#7007)

* fix(cli): multi-word and literal-wildcard search in odysseus-docs

odysseus-docs search wrapped the whole query in one `%{query}%` LIKE over title/current_content, so multi-word queries only matched the exact adjacent phrase and literal % / _ acted as SQL wildcards. Split on whitespace, AND per token, and escape LIKE wildcards with the shared escape_like_pattern() helper. Whitespace-only queries now match all documents, consistent with document-library search.

Fixes #2123.

Tests: tests/cli/test_docs_cli_search.py (5 tests; multi-word, %, _, backslash, whitespace); 4 fail pre-fix, 5 pass post-fix. tests/cli full suite: 82 passed.

* test(cli): name whitespace-query search test accurately

The test passes a whitespace-only query, not an empty one; the old name implied a different input. Behavior is unchanged.
This commit is contained in:
Merajul Arefin
2026-11-02 23:48:14 +00:00
committed by GitHub
parent c1f959ae61
commit f00ae350cd
3 changed files with 116 additions and 5 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ from tests.helpers.db_stubs import make_core_db_stub
def test_text_len_ignores_non_string_values(monkeypatch):
make_core_db_stub(monkeypatch, models=["Document", "DocumentVersion"])
make_core_db_stub(monkeypatch, models=["Document", "DocumentVersion", "ChatMessage", "Session"])
cli = load_script("odysseus-docs")
assert cli._text_len("hello") == 5
+103
View File
@@ -0,0 +1,103 @@
"""Regression for #2123 (CLI part): `odysseus-docs search` must match
multi-word queries and treat % / _ literally.
Old code built one ``%{query}%`` LIKE over title/current_content, so
"machine learning" only matched the exact adjacent phrase, and a literal
"%" matched every document.
"""
import json
from types import SimpleNamespace
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import NullPool
import core.database as cdb
from core.database import Document
from tests.helpers.cli_loader import load_script
def _cli_with_documents(monkeypatch, tmp_path):
engine = create_engine(
f"sqlite:///{tmp_path / 'docs_cli_search.db'}",
connect_args={"check_same_thread": False},
poolclass=NullPool,
)
cdb.Base.metadata.create_all(engine)
session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
db = session_factory()
try:
db.add_all(
[
Document(
id="doc-multiword",
title="ML notes",
current_content="machine notes on learning",
),
Document(
id="doc-percent",
title="Fabric",
current_content="100% cotton",
),
Document(
id="doc-underscore",
title="sale_2024",
current_content="spring campaign",
),
Document(
id="doc-backslash",
title="Path",
current_content=r"C:\photos\sunset",
),
]
)
db.commit()
finally:
db.close()
cli = load_script("odysseus-docs")
monkeypatch.setattr(cli, "SessionLocal", session_factory)
return cli
def _search_ids(cli, capsys, query):
cli.cmd_search(SimpleNamespace(query=query, limit=50, pretty=False))
return {doc["id"] for doc in json.loads(capsys.readouterr().out)}
def test_search_matches_words_in_any_order(monkeypatch, tmp_path, capsys):
cli = _cli_with_documents(monkeypatch, tmp_path)
assert _search_ids(cli, capsys, "machine learning") == {"doc-multiword"}
def test_search_escapes_percent_wildcard(monkeypatch, tmp_path, capsys):
cli = _cli_with_documents(monkeypatch, tmp_path)
assert _search_ids(cli, capsys, "%") == {"doc-percent"}
def test_search_escapes_underscore_wildcard(monkeypatch, tmp_path, capsys):
cli = _cli_with_documents(monkeypatch, tmp_path)
# Unescaped, "_" matches any single char so every document matches.
assert _search_ids(cli, capsys, "_") == {"doc-underscore"}
def test_search_escapes_backslash(monkeypatch, tmp_path, capsys):
cli = _cli_with_documents(monkeypatch, tmp_path)
assert _search_ids(cli, capsys, "\\") == {"doc-backslash"}
def test_search_whitespace_query_returns_all(monkeypatch, tmp_path, capsys):
cli = _cli_with_documents(monkeypatch, tmp_path)
assert _search_ids(cli, capsys, " ") == {
"doc-multiword",
"doc-percent",
"doc-underscore",
"doc-backslash",
}