-
Notifications
You must be signed in to change notification settings - Fork 17
Issue 102 Search Found Nothing For Hyphenated Values
A customer number like 05145-8841815, sitting in a spreadsheet attached to a ticket, could not be found by typing it. Typing the very same value with a space instead of the hyphen β 05145 8841815 β found it immediately.
Reported in issue #102 by dschipfel, with a table of seven searches and their results that made the diagnosis possible.
Fixed in b45f8a8d, released as update #1200.
The reporter listed exactly which searches worked and which did not:
| Typed | Result |
|---|---|
05145-8841815 |
no results |
051458841815 |
no results |
41815 |
no results |
05145,884 |
found |
05145.884 |
found |
05145 8841815 |
found |
05145 884 |
found |
Read that table again and the shape of it is odd: a comma works, a full stop works, a space works β and a hyphen does not. Those are all punctuation. Whatever was wrong could not simply be "punctuation confuses it".
Two things split text into words: the index, and the search box. They disagreed.
MySQL splits on punctuation. The text 05145-8841815 is stored in the index as two separate words:
05145 8841815
There is no entry for 05145-8841815, and none for 051458841815 either.
FreeITSM deleted the punctuation instead of splitting on it:
// includes/search/search.php β before
$clean = preg_replace('~[+\-><()\~*@"]+~u', '', $word);So 05145-8841815 became the single word 051458841815, and that is what was looked for:
+051458841815*
A word that exists nowhere in the index. The search was not confused β it did precisely what it was told, and correctly reported that nothing matched.
The strip list is + - > < ( ) ~ * @ " β the MySQL boolean operators. A comma is not on it. Nor is a full stop. They survived into the query and MySQL, which treats them as separators, split there itself. So the comma and full stop accidentally did the right thing, and the hyphen β being on the list β was destroyed.
That is the whole reason the table looks inconsistent. The behaviour depended on which punctuation mark happened to be an operator.
Quoted phrases are handled by a separate branch, which does the same substitution with one difference:
// the quoted-phrase branch β replaces with a SPACE
$p = trim(preg_replace('~[+\-><()\~*@"]+~u', ' ', $p));
// the word branch β replaced with NOTHING
$clean = preg_replace('~[+\-><()\~*@"]+~u', '', $word);' ' against ''. One character, nine lines apart. Searching for "05145-8841815" with quotes worked correctly the entire time.
Split on anything that is not a word character, rather than deleting it:
// after
$bits = preg_split('~[^\p{L}\p{N}_]+~u', (string)$word, -1, PREG_SPLIT_NO_EMPTY);Letters, digits and underscore are word characters, mirroring what InnoDB's own parser treats as one. Everything else β hyphen, dot, comma, slash, colon, apostrophe, and the boolean operators, which are ours to add rather than the user's to inject β is a separator.
before: 05145-8841815 -> +051458841815* no results
after: 05145-8841815 -> +05145* +8841815* found
A hyphenated value now reads exactly as if you had typed a space, which is what the reporter's own workaround already relied on. Every separator behaves the same way, so the inconsistency in that table is gone.
The leading minus is still the exclusion operator, and now applies to every part of the word it is attached to: -pre-flight becomes -pre -flight.
Quoting still demands adjacency, and is now the documented way to be precise:
"05145-8841815" -> +"05145 8841815"
Worth stating plainly, because both are limits of the search engine rather than decisions.
051458841815 β punctuation omitted entirely. The stored text contains a hyphen. Nothing in the index corresponds to the digits run together, so there is nothing to match. This could be made to work by also indexing a punctuation-stripped copy of every document, at the cost of a larger index and a full reindex of everything already stored. Not done.
41815 β the tail of a single word. MySQL full-text search matches the start of a word (884* finds 8841815) and never the end. There is no wildcard for it.
But the limit is narrower than the report supposed. A "latter part" that is a whole word works perfectly well: searching 789 finds GHI-789, because 789 is indexed as a word in its own right. It only fails when the fragment sits inside one word, as 41815 does inside 8841815.
The practical workaround for a long number is to type the part you know from its beginning: 05145 884 finds it.
| File | What changed |
|---|---|
includes/search/search.php |
searchParseQuery() splits on non-word characters instead of deleting them. |
tests/search/run.php |
Eight new cases. |
| The indexer | It never had this fault. It stores the text as it is; MySQL does the splitting. |
| The quoted-phrase branch | Always substituted a space. "05145-8841815" worked throughout. |
Reproduced first. A fixture containing the reporter's exact value, run against all seven of their search terms, produced their table line for line β including pre-flight, which they noted as the same fault in ordinary words.
End to end on a real emailed attachment. An Excel file was emailed into a test installation, arriving as TICKET-000108, containing ABC-123 DEF-456 GHI-789 XXX-YYY-ZZZ. Searched through the real query path with real scoping:
| Search | Before | After |
|---|---|---|
ABC-123 |
+ABC123* β nothing |
+ABC* +123* β found |
DEF-456 |
+DEF456* β nothing |
+DEF* +456* β found |
XXX-YYY-ZZZ |
+XXXYYYZZZ* β nothing |
+XXX* +YYY* +ZZZ* β found |
That last one matters: a value with two hyphens splits into three parts correctly.
The test suite. tests/search/run.php went from 44 passing to 52, the eight new cases covering the split itself, every separator behaving alike, the leading minus across a hyphenated word, operators embedded mid-word, and a fragment below the index minimum being dropped rather than required.
That last one guards a real trap the suite already knew about: requiring a word shorter than the index minimum makes the entire query match nothing, so B4-05145 must search for 05145 alone rather than demanding a B4 that was never indexed.
Type a reference with hyphens in it and it will be found β in ticket text, notes, and the contents of attached documents.
If you want to be precise, put quotes round it: "05145-8841815" requires the parts to appear together, in that order, rather than merely somewhere in the same document.
If you only know part of a long number, type it from the start (05145 884) rather than the end.
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)