From dddda9a9f862435767871c46c2e93119b7c09951 Mon Sep 17 00:00:00 2001 From: wanjinhao1 <394959433@qq.com> Date: Thu, 5 Nov 2026 04:08:25 +0800 Subject: [PATCH 1/2] Fix #3538: document quoted Lucene exact-match form and token path for body text in MCP descriptions --- packages/api/src/mcp/prompts/dashboards/content.ts | 14 ++++++++------ packages/api/src/mcp/tools/query/schemas.ts | 7 +++++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/api/src/mcp/prompts/dashboards/content.ts b/packages/api/src/mcp/prompts/dashboards/content.ts index 69c8f4eb17..e7af99da67 100644 --- a/packages/api/src/mcp/prompts/dashboards/content.ts +++ b/packages/api/src/mcp/prompts/dashboards/content.ts @@ -881,11 +881,13 @@ Lucene field:value does NOT mean exact equality. It translates to ilike(field, ' ServiceName:api* does NOT mean "starts with api". It means "contains a substring starting with api". In practice this often works for prefix matching, but it is NOT equivalent to SQL LIKE 'api%'. - For exact matching or true prefix/suffix/contains patterns, use SQL instead: - Exact: whereLanguage: "sql", where: "SpanKind = 'Server'" - Prefix: whereLanguage: "sql", where: "ServiceName LIKE 'api%'" - Contains: whereLanguage: "sql", where: "Body LIKE '%timeout%'" - Regex: whereLanguage: "sql", where: "match(ServiceName, '^api-v[0-9]+')" + For exact matching or true prefix/suffix/contains patterns: + Exact (stay in Lucene, QUOTE the value): SpanKind:"Server" (compiles to SpanKind = 'Server', uses the primary key) + Exact (SQL): whereLanguage: "sql", where: "SpanKind = 'Server'" + Prefix: whereLanguage: "sql", where: "ServiceName LIKE 'api%'" + Body text: prefer a bare Lucene term (timeout) or SQL hasToken(lower(Body), 'timeout') over LIKE '%timeout%' -- + the token path uses the text index; Body:term still compiles to substring ILIKE and scans the whole column. + Regex: whereLanguage: "sql", where: "match(ServiceName, '^api-v[0-9]+')" When Lucene substring matching is FINE: free-text columns like Body, StatusMessage where substring is the intent. When Lucene substring matching is WRONG: enum-like columns (SpanKind, SeverityText, StatusCode, ServiceName) where you want exact or true prefix match. @@ -901,7 +903,7 @@ Lucene queries that reference map-attribute keys via dotted paths (http.status_c Reliable in Lucene (top-level columns only): Duration:>1000000000 (top-level numeric range) - ServiceName:checkout (top-level string -- but remember this is a substring match, not exact!) + ServiceName:"checkout" (top-level string, quoted for exact match; unquoted ServiceName:checkout is substring!) Workaround: switch to SQL with bracket access. Map values are stored as strings, so quote the comparison value. diff --git a/packages/api/src/mcp/tools/query/schemas.ts b/packages/api/src/mcp/tools/query/schemas.ts index f274f7e893..03db726dde 100644 --- a/packages/api/src/mcp/tools/query/schemas.ts +++ b/packages/api/src/mcp/tools/query/schemas.ts @@ -20,7 +20,8 @@ const WHERE_DESCRIPTION = 'WRONG: level = "error" (SQL syntax with whereLanguage:"lucene")\n\n' + 'SUBSTRING TRAP: Lucene field:value matches ANY row containing "value" as a substring, not exact equality.\n' + ' SpanKind:Server matches "Server", "ServerStreaming", "InternalServer", etc.\n' + - " For exact match, use SQL: SpanKind = 'Server'"; + ' For an exact match stay in Lucene and QUOTE the value: SpanKind:"Server" (= SpanKind = \'Server\').\n' + + " For exact match in SQL: SpanKind = 'Server'"; const WHERE_LANGUAGE_DESCRIPTION = 'Query language for the "where" filter. Default: lucene.\n' + @@ -31,7 +32,9 @@ const WHERE_LANGUAGE_DESCRIPTION = 'and map attributes via dot notation. Use "sql" for IN(...) lists, complex expressions, or function calls.\n' + 'IMPORTANT: Lucene field:value is a SUBSTRING match (ilike), not exact equality. ' + 'field:val* is prefix-within-substring, not a true prefix match. ' + - "For exact matching or reliable wildcards, use SQL: WHERE field = 'value' or WHERE field LIKE 'val%'.\n\n" + + 'For an exact match in Lucene, quote the value: field:"value" compiles to field = \'value\' and can use the primary key. ' + + "For reliable wildcards, use SQL: WHERE field = 'value' or WHERE field LIKE 'val%'.\n" + + "For free-text body/message columns, prefer a bare Lucene term (e.g. timeout) or SQL hasToken(lower(Body), 'timeout') over LIKE: they use the text index instead of scanning the whole column.\n\n" + 'Common mistake: writing Column:value (Lucene) but setting whereLanguage to "sql". ' + 'If your filter uses colon syntax, leave whereLanguage as "lucene" (the default).'; From 7b8dde004e17f9ff4cbad31b124256aea83b1f82 Mon Sep 17 00:00:00 2001 From: wanjinhao1 <394959433@qq.com> Date: Thu, 5 Nov 2026 04:10:01 +0800 Subject: [PATCH 2/2] Add changeset for MCP Lucene exact-match docs --- .changeset/mcp-lucene-exact-match-docs.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/mcp-lucene-exact-match-docs.md diff --git a/.changeset/mcp-lucene-exact-match-docs.md b/.changeset/mcp-lucene-exact-match-docs.md new file mode 100644 index 0000000000..99bc06bd93 --- /dev/null +++ b/.changeset/mcp-lucene-exact-match-docs.md @@ -0,0 +1,5 @@ +--- +'@hyperdx/api': patch +--- + +docs: Teach the quoted Lucene exact-match form (field:"value") and the bare-term/hasToken body-text path in MCP tool descriptions