How do you normalize service names before semantic matching?

Hi everyone,

I’m working on a project that involves matching businesses based on the services they provide. One challenge I’ve noticed is that different companies often describe the same service using different terms.

For example:

  • Junk removal vs. debris hauling
  • Tree trimming vs. tree pruning
  • Pressure washing vs. power washing
  • House cleanout vs. estate cleanout

A simple keyword match misses many of these relationships, while pure semantic search sometimes returns services that are only loosely related.

I’m curious how others approach this problem.

  • Do you maintain a synonym dictionary?
  • Do you normalize service names before generating embeddings?
  • Have you had better results with taxonomy-based matching or embedding-based retrieval?
  • How do you handle services that partially overlap?

I’d appreciate hearing about real-world approaches that have worked well.

Hmm. Looking at existing systems, it seems this is often handled by combining several components rather than relying on a single method:


I have not implemented this exact service-matching use case, but the adjacent systems I found seem to converge on roughly the same separation of responsibilities.

My direct answers would be:

  1. A synonym dictionary can help, but I would make it a concept-centered alias registry rather than a collection of pairwise rewrite rules.
  2. Light normalization before embedding is useful, but I would preserve the original label and avoid collapsing meaningful modifiers too early.
  3. Taxonomy and embedding retrieval are complementary, not competing alternatives: the taxonomy defines the possible concepts, while lexical and embedding methods retrieve candidates from it.
  4. Partial overlap should not automatically become synonymy. It is usually safer to retain a relation such as exact, broader, narrower, related, rejected, or unknown.

A practical default pipeline might look like this:

raw service label
+ business category / description / other available context
        ↓
light deterministic cleanup
        ↓
exact-alias, lexical, and dense candidate retrieval
        ↓
task-specific disambiguation or relation decision
        ↓
exact / related / reject / unknown
        ↓
downstream business-matching policy

The most important distinction may be what “match” means downstream:

Desired result Reasonable treatment
Same canonical service Use conservative exact or close mappings and reject unresolved ambiguity
Useful search expansion Broader, narrower, and related services can remain candidates, but should retain their relation labels
Evidence that a provider can perform the requested work A service name alone may be insufficient; category, description, scope, location, audience, equipment, or other capability fields may matter

That distinction determines the labels, evaluation set, and threshold policy more than the embedding model does.

For a first implementation, I would probably start with:

  • a small, versioned service registry;
  • stable service IDs;
  • preferred labels and aliases;
  • a short definition or scope note;
  • optional parent and related-service links;
  • an explicit unknown or needs_review result;
  • a small evaluation set containing both clear synonyms and deliberately confusing near-neighbors.

Even a modest hand-reviewed set can answer more useful questions than comparing models on undifferentiated examples:

  • Does the correct concept appear in the top K candidates?
  • Can the final stage distinguish the same service from a related service?
  • How often is a taxonomy-external input forced into a plausible but wrong concept?
  • What fraction can be accepted automatically, reviewed, or rejected?
Why I would separate the registry, retrieval, and final decision

The original question combines several problems that look similar at the surface but have different contracts.

1. Concept inventory

First, there needs to be some target concept space:

service_id
preferred_label
aliases
definition
scope_note
parent_ids
related_ids
status
version
provenance

The important part is that the ID represents the concept, not whichever text label happens to be preferred today.

This is close to the general model used by controlled vocabularies. The W3C SKOS reference separates concepts from labels and provides vocabulary for preferred labels, alternate labels, definitions, scope notes, broader/narrower relationships, and related concepts.

You do not need to implement RDF or adopt SKOS literally. Its useful contribution here is the separation:

concept identity
≠ preferred display name
≠ alternate label
≠ relationship to another concept

The Open Referral HSDS guidance provides a service-oriented example of the same general structure. It models services separately from taxonomy terms; taxonomy terms can have codes, names, descriptions, and hierarchical parents, and a service can be associated with one or more classifications.

2. Candidate generation

Exact aliases, lexical search, fuzzy matching, sparse retrieval, and dense retrieval can all be useful here.

Their job is not necessarily to decide that two services are equivalent. Their lower-risk job is:

Given this input, which canonical concepts deserve closer inspection?

Different retrieval methods cover different failure modes:

Candidate method Often useful for Typical failure
Exact alias Known, curated variants Ambiguous aliases and unseen variants
Character or token matching Spelling, inflection, shared terminology Semantically equivalent labels with little lexical overlap
Dense embedding Paraphrases and lexically distant candidates Related concepts appearing as if they were equivalent
Hybrid retrieval Combining complementary candidate sets Fusion method and weighting still require evaluation

The usual information-retrieval pattern is to retrieve generously and then evaluate a smaller candidate set more strictly. The Sentence Transformers retrieve-and-rerank example illustrates this general architecture.

However, its standard CrossEncoder examples estimate query-document relevance. A service-normalization decision may instead require labels such as:

same concept
broader concept
narrower concept
related but not substitutable
unrelated
insufficient context

A generic search reranker does not automatically learn those distinctions. A second stage could therefore be:

  • deterministic rules;
  • a pair classifier trained on service relations;
  • a reranker fine-tuned on domain examples;
  • taxonomy constraints;
  • selective human review;
  • or a mixture of these.

3. Final matching decision

Candidate score and final decision should remain separate outputs.

The current Reconciliation Service API draft makes a similar separation between:

  • candidate identity;
  • an optional score;
  • individual matching features;
  • and a Boolean match decision.

It also allows contextual properties and types to be supplied with the name. That is useful here because a short service label may be ambiguous by itself, while category or description can make it resolvable.

A candidate record might therefore look more like:

{
  "service_id": "example:123",
  "preferred_label": "Example service",
  "relation": "related",
  "retrieval_score": 0.81,
  "decision": "review",
  "features": {
    "alias_match": false,
    "category_match": true,
    "description_similarity": 0.76,
    "parent_match": true
  }
}

This makes debugging and later policy changes much easier than storing only a normalized string and one cosine value.

Alias ambiguity and partial overlap

A synonym dictionary is still useful, but two edge cases seem important.

One alias may identify multiple concepts

Short occupational titles, product attributes, medical terms, and service names can all be context-dependent.

For example, a generic label such as installation, cleanup, repair, or consulting may legitimately appear under several concepts. Even an exact alias match does not prove uniqueness unless the registry guarantees that alias is unambiguous in the relevant category.

A safer registry permits something like:

alias
  → candidate concept A
  → candidate concept B

and then uses context to disambiguate.

Possible contextual fields include:

  • business category;
  • service description;
  • target object;
  • customer or audience;
  • residential versus commercial;
  • location or service area;
  • emergency versus scheduled work;
  • equipment, certification, or constraints.

This also means an evaluation dataset should not force every ambiguous alias into one arbitrary “correct” ID. It can instead use:

  • a set of acceptable concepts;
  • a relation label;
  • or needs_context.

Similarity chains should not become automatic synonym chains

Suppose:

A is close to B
B is close to C

That does not necessarily imply that A and C are interchangeable.

The distinction is explicit in SKOS. It provides relations such as:

  • exactMatch;
  • closeMatch;
  • broadMatch;
  • narrowMatch;
  • relatedMatch.

In particular, “close” and “related” relationships are not intended to behave like unrestricted transitive identity.

You may not need all of those labels initially. A useful MVP could be:

exact
related
reject
unknown

and later expand related into:

close
broader
narrower
overlapping

only if the downstream application actually treats those cases differently.

Do not decide the example pairs without domain policy

Pairs such as:

  • tree trimming / tree pruning;
  • junk removal / debris hauling;
  • house cleanout / estate cleanout;

may be synonyms in one marketplace, related specializations in another, or operationally non-substitutable under certain provider constraints.

That is not merely a language-model question. It is partly a domain-policy question:

Under what conditions should the system treat two services as interchangeable for this product?

Encoding that policy explicitly is likely to be more stable than expecting one similarity threshold to infer it.

How much normalization to perform before embedding

I would separate surface cleanup from semantic canonicalization.

Usually low-risk before retrieval

  • Unicode normalization;
  • case normalization;
  • whitespace normalization;
  • punctuation normalization;
  • obvious spelling corrections;
  • carefully curated abbreviation expansion;
  • singular/plural handling where appropriate.

Potentially destructive before the relation decision

  • deleting domain modifiers;
  • replacing one service with a broader parent;
  • replacing a specialization with a generic service;
  • automatically expanding all related terms as synonyms;
  • discarding the original provider wording.

For example, modifiers such as these may carry the distinction that the matcher eventually needs:

estate
commercial
residential
emergency
mobile
industrial
licensed
hazardous
interior
exterior

I would therefore retain at least:

raw_label
cleaned_label
candidate_concepts
chosen_relation
normalization_version

It can also be useful to embed multiple concept representations separately:

preferred label
preferred label + definition
known aliases
preferred label + category + scope

Then evaluate which representation improves candidate recall without turning related concepts into false equivalents.

The O*NET Alternate Titles automation report is a useful adjacent example. O*NET separates relatively deterministic processing, such as acronym and abbreviation standardization, from harder decisions involving vague titles, occupation mismatches, context differences, and level mismatches. Its process combines dictionary logic, search ranking, semantic similarity, rules, and analyst validation rather than treating all normalization as a single embedding operation.

This is an occupational taxonomy rather than a service taxonomy, so its thresholds and performance should not be transferred directly. The architectural separation is still informative.

Unknown services and the free-form tail

A taxonomy is unlikely to contain every service that providers will enter.

If the system always chooses the nearest canonical concept, a taxonomy-external input can still receive a very plausible score. That produces a particularly difficult failure mode: the result looks confident, is semantically related, and is nevertheless the wrong normalization.

For that reason, I would make these first-class outcomes:

matched
related candidate
unknown / no suitable concept
needs review

A single global cosine threshold may be a useful baseline, but I would not assume that it cleanly separates:

  • correct known concepts;
  • close but incorrect concepts;
  • ambiguous aliases;
  • taxonomy-external inputs.

Useful additional signals can include:

  • top-1 score;
  • top-1 minus top-2 margin;
  • exact or lexical evidence;
  • category compatibility;
  • relation classifier output;
  • agreement among retrieval methods;
  • whether contextual fields support the same candidate;
  • whether the candidate is a parent, sibling, or related concept.

A real production data model can also retain standardized concepts and free-form services side by side.

For example, the Google Business Profile ServiceList API distinguishes Google-defined structured services, identified by serviceTypeId, from merchant-entered free-form services that are not exposed in the structured service data.

That does not reveal Google’s internal matching algorithm, but it is a useful design precedent: not every provider label has to be forced immediately into the standardized inventory.

Unknown labels can also become taxonomy-maintenance input:

frequent unknown
        ↓
cluster and review
        ↓
new alias, new concept, or explicit rejection rule
        ↓
versioned registry update
A small evaluation plan before choosing the model

I would build a small stratified set before spending much time selecting embedding models or tuning one threshold.

Suggested groups

Group What it tests
Clear aliases Basic known normalization
Lexically distant aliases Semantic candidate retrieval
Ambiguous aliases Need for context or multiple candidates
Close siblings Relatedness versus equivalence
Parent-child pairs Hierarchical relation handling
Partial-overlap pairs Whether binary synonymy is too coarse
Same-category hard negatives Difficult false positives
Unknown services Rejection and taxonomy coverage
Modifier-sensitive pairs Whether preprocessing removes important meaning

The nearby wrong candidates are especially valuable. Sentence Transformers calls these hard negatives: examples that appear similar but are not correct for the task.

Measure the stages separately

Candidate retrieval

Recall@1
Recall@5
Recall@10

Question:

Was an acceptable concept present among the retrieved candidates?

Final relation decision

Use a confusion matrix over whichever labels you adopt:

exact
broader
narrower
related
reject
unknown

Question:

Once the right candidate was available, did the system assign the correct relation?

Unknown handling

Measure:

unknown false-accept rate
known false-reject rate
review coverage

Question:

When the correct concept was absent, did the system abstain or select a plausible wrong concept?

Operational coverage

automatically accepted
sent to review
rejected

This makes it possible to compare policies, not just models.

For example:

Policy Likely trade-off
Conservative automatic matching Higher precision, more review
Search-oriented expansion Higher recall, more related results
Provider recommendation Requires more context and downstream constraints

A useful threshold is therefore not necessarily the one with the best overall accuracy. It is the one that fits the cost of the downstream error.

Avoid one misleading aggregate score

A system can achieve apparently good overall accuracy if the dataset contains mostly easy aliases, while still failing on the exact cases motivating the post.

I would report the groups separately, especially:

  • unambiguous aliases;
  • ambiguous aliases;
  • close-but-distinct concepts;
  • unknown inputs.

Also, if an alias legitimately maps to multiple concepts, evaluate against the acceptable set or mark it as requiring context. Otherwise the benchmark will classify some reasonable candidates as model errors merely because it imposed a false single-answer assumption.

A small adjacent-domain sanity check

As a quick proxy, I tried a small held-out-title experiment using the current O*NET occupational registry and its alternate job titles.

This is not a service-domain benchmark, and it cannot determine whether any of the pairs in the original post are equivalent. It was only meant to test whether the expected failure modes appear in a mature adjacent taxonomy.

A few observations were consistent with the layered design above:

  • adding definitions or alias information mainly helped put the correct concept somewhere in the candidate list;
  • high-scoring incorrect dense matches were frequently officially related occupations rather than random occupations;
  • some exact alternate-title strings were attached to more than one occupational concept;
  • after deliberately removing the true concept from the candidate inventory, the nearest remaining concept could still receive a convincing similarity score;
  • a simple lexical/dense fusion was not automatically better than each component, even though the two methods recovered different correct cases.

I would interpret that only as a sanity check:

candidate retrieval, ambiguity resolution, relation classification, and unknown rejection are distinct problems.

It does not establish a service-domain model choice or production threshold.

Conditional implementation paths

A possible decision flow is:

If you already have a suitable taxonomy

  1. Keep stable IDs, preferred labels, aliases, definitions, scope notes, and hierarchy.
  2. Preserve the raw provider label.
  3. Retrieve candidates using exact, lexical, and/or dense methods.
  4. Decide the relation in a separate stage.
  5. Retain unknown and review.
  6. Evaluate candidate recall and final decisions separately.

If you have a taxonomy, but it was built for a different purpose

Check whether its granularity matches the downstream decision.

A statistical, procurement, or directory taxonomy may be authoritative for its own purpose while still being too broad, too narrow, or differently organized for provider matching.

In that case, possible options are:

  • use it as a high-level backbone;
  • add an application-specific layer beneath it;
  • map an internal service registry to it;
  • or use it only as one contextual feature.

If you do not have a taxonomy

Start smaller than a full ontology:

frequent canonical concepts
+ curated aliases
+ short scope notes
+ explicit unknown/free-form storage

Grow it from observed provider labels and reviewed failures.

This keeps the initial work proportional to actual usage and avoids designing an enormous hierarchy before knowing which distinctions matter.

If false positives are expensive

Prefer:

exact or strongly supported match
otherwise review / unknown

Use conservative automatic acceptance and preserve related candidates for inspection.

If recall is more important

Return a wider candidate set with explicit relation labels rather than silently declaring every result equivalent.

If you have labeled relation pairs

A task-specific pair classifier or reranker becomes worth testing.

Train and evaluate it on difficult cases:

  • same concept;
  • close sibling;
  • broader or narrower;
  • related but not substitutable;
  • unknown.

If you only have positive synonym pairs

Mine or manually add hard negatives before training. Otherwise the model may learn general topical relatedness rather than the boundary between equivalent and merely related services.

So my default answer would be:

Use a taxonomy or small concept registry as the destination.
Use an alias dictionary for high-confidence known variants.
Use lexical and embedding retrieval to generate candidates.
Use a separate task-specific step to determine the relation.
Preserve unknowns and ambiguous cases instead of forcing a match.
Evaluate retrieval, relation decisions, and rejection separately.

That seems closer to how existing reconciliation and taxonomy systems are structured than searching for one universal service-name normalizer.

The two implementation details that would change the route most are:

  1. whether “match” means the same canonical service, useful search expansion, or actual provider capability; and
  2. which contextual fields are available beyond the short service label.

Everything else—model choice, thresholds, relation granularity, and review policy—can be selected downstream of those decisions.

I agree with the separation between candidate retrieval and the final matching decision. One issue I’ve seen with service names in practice is that the wording used by a business often reflects how customers describe the service rather than how the underlying service is actually categorized.

For example, two businesses might use different labels for essentially the same type of work, while another pair of labels may look very similar but represent different scopes. That makes a simple embedding-similarity threshold risky if the downstream system is expected to determine actual service capability.

I’d probably keep the original provider wording alongside the normalized concept rather than replacing it completely. That gives you something like:

raw label → candidate concepts → relation → canonical service

and makes it possible to revisit questionable matches later.

I also like the idea of treating context as part of the matching decision. In a real service directory, things such as residential vs. commercial, the object being serviced, service area, and the actual scope of work can change whether two apparently similar labels should be treated as equivalent.

A practical example is the difference between broad service labels and the individual things a provider actually accepts. Looking at real-world service examples can make those boundary cases much easier to identify than testing only short synthetic labels.

I’d also be careful with terms like “cleanup,” “removal,” or “hauling.” They can have substantial lexical overlap while representing different operational capabilities. I’d rather classify them as related and retain the relationship than automatically collapse them into one canonical concept.

For evaluation, I’d especially build hard negatives around these cases: same concept with different wording, related services, broader/narrower services, and services that share important words but are not interchangeable. That seems much more informative than measuring overall similarity accuracy on easy synonym pairs.

So I’m with you on the main architecture: normalization should help candidate generation, not prematurely make the semantic decision. Keeping the raw label, retrieved candidates, relation type, confidence, and final decision separately would also make the system much easier to debug.

That’s true. Real-world cases can reveal things that are easy to miss if you look only at the theory.


I think your example suggests one more boundary that may be worth making explicit: resolving the service concept and deciding actual provider capability are related, but they are not quite the same decision.

A compact version of the pipeline might be:

raw provider wording + context
        ↓
candidate concepts
        ↓
concept relation
        ↓
capability / applicability checks
        ↓
accept / reject / unsure

That seems especially useful for the kind of cases you pointed to. A label can be semantically close to the right canonical concept while still being a bad operational match because of the object involved, residential vs. commercial scope, geography, eligibility, delivery mode, or some other constraint.

The junk-removal example you linked is actually a nice illustration of this distinction. I would not treat that commercial page as evidence for a particular matching algorithm, but it is useful requirements/stress-case evidence: the provider itself distinguishes items that are accepted normally, accepted with special handling, and not accepted. Even within something as apparently simple as “appliance removal,” washers/dryers and refrigerant-containing appliances have different handling requirements, while other superficially related items are rejected entirely.

So if the goal is actual service capability rather than only semantic discovery, I would probably avoid asking one embedding score to represent all of those distinctions.

The lowest-cost version I can think of would be:

  1. Keep the original wording and context. Do not overwrite it with the normalized label.
  2. Use embeddings primarily to retrieve a small candidate set.
  3. Record the relation to each promising candidate separately — e.g. exact/close, broader, narrower, related, or none/unknown.
  4. Apply capability checks only where reliable attributes already exist. A structured service-area field can be a filter; a residential/commercial flag can be a feature or filter; missing information can stay unknown rather than being inferred from the service name.
  5. Allow UNSURE / no-valid-match. Do not force the nearest candidate to become the canonical answer.

That does not require replacing the current retrieval model. Most of the gain would initially come from separating states that otherwise get compressed into the same similarity number.

There is a useful precedent for the mechanics in the W3C Entity Reconciliation Community Group’s current Reconciliation Service API draft. It keeps a candidate’s score, individual matching features, and the final Boolean match decision separate. Query properties can also be required filters or merely affect ranking, and the spec explicitly notes that some conditions, such as geographic containment, may be naturally binary rather than meaningfully represented by a similarity score.

That is not a service-directory algorithm by itself, but the separation is a useful one here:

retrieval score      = "how promising is this candidate?"
relation             = "how are these concepts related?"
capability evidence  = "does this provider satisfy the relevant constraints?"
final decision       = "should this be treated as a usable match here?"

I also think your hard-negative idea becomes even more useful if the test set is treated as a boundary set rather than making every difficult example a negative. Some of the hardest cases are actually positives or partial relations.

For example:

Case What it tests
same service, very different wording retrieval/paraphrase robustness
same or nearly same scope equivalence decision
broader vs. narrower service granularity/relation
related sibling services relatedness without collapsing
strong lexical overlap, different capability false-positive resistance
same concept, incompatible service area/audience/object capability stage
no appropriate canonical target abstention / forced-match behavior
vague bundled record vs. several granular records representation/granularity

That would let errors be assigned to the stage that actually produced them instead of only reporting whether the final pair crossed one threshold.

Why I think granularity is a real part of this problem

There are some surprisingly close examples in the human-service-directory world.

Connect211 and Open Referral recently described an open-source Record Matcher used to compare resource directories. Their problem is entity resolution rather than exactly the service-normalization problem here, so I would not assume the causes are identical. But the service-level failure modes are relevant.

They describe cases where essentially the same cluster of activities is represented as one service record in one directory and five records in another. For service resolution they therefore use multiple kinds of evidence — factual fields, categories/taxonomies, and semantic similarity in names/descriptions — rather than name similarity alone.

That is a useful reminder that a mismatch can come from at least three different places:

different wording
different concept
different record granularity

Those are easy to conflate if all of them ultimately appear as “low/high cosine similarity.”

There is an even more direct granularity discussion in a 2026 Open Referral community thread on describing data quality. One example asks what level should count as the service:

Welfare support
Food bank
Vegetarian food bank
Vegan food bank
Kosher foodbank
Halal foodbank

The point made there is that the useful granularity depends on the user need.

The same discussion gives a nice bundled/unbundled example: a large authority represents a library as one service, while the local township represents the same library as roughly ten to twelve individual services. They argue that preserving granular source information is valuable because reliably aggregating several specific records later is generally easier than reconstructing several specific services from one vague record.

That makes your suggestion to retain the provider’s original wording look important for more than debugging. It helps avoid making normalization an irreversible information-loss operation.

I would therefore be inclined to store something conceptually like:

{
  "raw_label": "...",
  "raw_context": "...",
  "candidate_concepts": [...],
  "chosen_relation": "...",
  "canonical_concept": "...",
  "capability_evidence": {...},
  "decision": "...",
  "decision_reason": [...]
}

Not necessarily this exact schema — just the separation.

It also means that before changing an embedding model, one cheap sanity check may be to inspect the most frequently confused taxonomy siblings manually:

Can two humans distinguish these concepts from the definitions and available context?

If the boundary itself is unclear, changing the embedding threshold may only move the error around.

Relation types: useful vocabulary without making the ontology too heavy

Your raw label → candidate concepts → relation → canonical service formulation also resembles how some established crosswalk systems separate semantic suggestions from the final relationship.

For example, the European Commission’s ESCO–O*NET crosswalk used transformer embeddings and cosine similarity to generate a ranked list of candidate occupations. Human validators then determined the relationship between the concepts using categories such as exact, broad, narrow, and close; a published variant also includes related mappings.

That is a different domain — occupations are much more standardized than provider-written service descriptions — but the pipeline shape is interesting:

embedding similarity
        ↓
candidate suggestion
        ↓
relation validation

rather than:

embedding similarity > threshold
        ↓
same concept

SKOS supplies related vocabulary:

  • exactMatch
  • closeMatch
  • broadMatch
  • narrowMatch
  • relatedMatch

I would use that more as a vocabulary/reference point than as a requirement to implement SKOS itself.

One useful detail is that SKOS deliberately distinguishes closeMatch from identity: it defines it in terms of concepts being sufficiently similar to be interchangeable in some information-retrieval applications. That is already weaker than saying two services have identical operational capability.

So for this use case there may really be two separate questions:

concept relation:
    exact / close / broader / narrower / related / none

operational applicability:
    yes / no / conditional / unknown

For example:

"refrigerator removal"
        concept relation to "appliance removal":
            narrower

        provider capability:
            conditional (special refrigerant handling)

or:

"biohazard cleanup"
        concept relation to "junk removal":
            related

        provider capability:
            no

That seems more faithful than trying to encode both facts into a single “0.78 similar” value.

I would also keep confidence scoped. A retrieval score, confidence in the concept relation, and confidence in the final capability decision are not necessarily the same quantity.

Something like:

retrieval_score
relation
relation_confidence
capability_status
final_decision

is easier to interpret later than a single generic confidence = 0.82.

Again, I would not necessarily implement all of those fields on day one. The main value is keeping the meanings distinct.

A cheap capability layer: structured first, extraction later

I think this is where the real-world cases are particularly useful.

A reasonable default could be:

Is the condition already structured?
    |
    +-- yes --> use it directly as a filter/feature where appropriate
    |
    +-- no --> is it reliably present in free text?
                 |
                 +-- yes --> preserve the text; optionally extract later
                 |
                 +-- no --> leave capability UNKNOWN

For example:

service_area structured?
    -> cheap deterministic geographic check

residential/commercial structured?
    -> cheap compatibility feature/filter

age/eligibility structured?
    -> cheap compatibility check

accepted/excluded object explicitly structured?
    -> cheap capability check

only implied by marketing copy?
    -> do not pretend the canonical label answered it

I would resist making every contextual field a hard gate. Some attributes are hard constraints, while others should merely influence ranking.

The W3C reconciliation draft has a nice generic distinction here: a property can be marked required, in which case it acts like a filter, or optional, in which case it affects the candidate ordering. Its matchQualifier mechanism can also represent different relations rather than assuming equality.

The practical service version might be:

HARD / near-hard
- outside service area
- explicit "commercial only"
- explicit exclusion
- eligibility definitely not satisfied

SOFT
- preferred specialization
- distance
- likely but not guaranteed scope
- customer wording similarity

UNKNOWN
- information not present

The key point is that unknown should not silently turn into either true or false.

That is also why I would start with attributes the data already contains. Trying to immediately extract every capability dimension from arbitrary provider prose with an LLM would turn a relatively small architecture improvement into a second large NLP problem.

If later error analysis shows that one missing modifier is responsible for many failures — e.g. residential/commercial or emergency/non-emergency — then extracting that one field becomes a much more targeted investment.

Evaluation: separate retrieval failures from decision failures

The public Connect211 training/tuning notes are useful here because they explicitly diagnose errors by pipeline stage.

Their exact task is duplicate/entity resolution, so the labels are not directly transferable, but the diagnostic idea is.

They distinguish, for example:

not a candidate
    -> blocking/retrieval failure

candidate, but below decision threshold
    -> scoring/decision failure

candidate scored sufficiently, but later vetoed
    -> mitigation/gating failure

They also recommend building labeled ground truth before parameter tuning, sample different score bands, inspect near-misses below the threshold, and change one variable at a time.

For the service-matching pipeline, I think the analogous evaluation could be quite small:

1. Candidate retrieval

Question:

Did the appropriate concept or useful related concept survive into top-k?

Possible metrics:

Recall@1
Recall@5
Recall@10

The important metric is probably not “average cosine similarity.” It is whether a correct/useful candidate was available to the next stage.

2. Relation classification

Given the relevant candidate, did the system call it:

exact
close
broader
narrower
related
none

correctly enough for the downstream use?

A confusion matrix here would reveal something that a single global matching accuracy hides — e.g. perhaps exact vs. related is the real problem.

3. Capability/applicability

Given an acceptable concept relation, did contextual evidence reverse the operational decision when it should?

For example:

same broad service concept
+ wrong geography
= do not return as capable provider

same broad service concept
+ explicitly excluded object
= do not return as capable provider

narrower service
+ compatible request
= may be an excellent match

4. Abstention

Include cases where the correct answer is:

no canonical target
insufficient evidence
needs review

Then measure how often the system forces one of them into the nearest known concept.

This seems particularly important for semantic embeddings because there is always a nearest vector even when no good semantic answer exists.

5. Boundary set

Rather than immediately creating a huge benchmark, a manually reviewed set of perhaps a few dozen recurring boundary cases may tell you much more.

I would include at least:

A. Same concept / very different customer wording
B. Same concept / one extra modifier
C. Broader vs. narrower
D. Related siblings
E. Heavy lexical overlap / genuinely different service
F. Same concept / incompatible capability
G. Bundled vs. granular record
H. No valid target

Then each future model/threshold/policy change can run against the exact same cases.

That turns “this embedding model feels better” into something much easier to debug.

One caution on hard-negative mining

I agree with the direction of your hard-negative suggestion, but there is one trap I would explicitly guard against: a semantically close neighbor is not automatically a negative.

That is especially dangerous in a hierarchy.

Suppose the taxonomy contains:

cleanup
├── estate cleanup
├── construction cleanup
└── biohazard cleanup

If a training script blindly takes the nearest sibling as a hard negative, some examples may actually be valid broader/narrower/related matches depending on the request and intended output.

There is a recent analogous example in the paper Fine-Grained Curriculum Standards Alignment on the MathFish Benchmark. The task is education rather than services, but the authors specifically note that sibling standards sampled as hard negatives can occasionally be true positives, which can teach the bi-encoder to over-suppress genuinely related standards.

So I would probably mine difficult neighbors automatically, but review the boundary labels before using them as negatives.

In other words:

nearest difficult neighbor
        ↓
manual/reliable relation label
        ├── exact/close       -> positive
        ├── broader/narrower  -> structural relation
        ├── related           -> related case
        ├── capability-only mismatch
        └── true negative

This is one reason I like “boundary set” as the broader evaluation concept. Not every useful difficult pair needs to become a contrastive negative.

What I would probably not change first

Given the architecture you described, I would not start by:

  • fine-tuning a new embedding model,
  • building a large custom ontology,
  • adding an LLM judge to every pair,
  • extracting every possible capability field,
  • or searching for one globally optimal cosine threshold.

Those may eventually be useful, but they make several variables move at once.

The cheaper sequence seems more diagnostic:

1. Preserve raw wording/context.
2. Freeze the current retriever as a baseline.
3. Save top-k candidates and retrieval scores.
4. Add a small relation label.
5. Add only capability checks supported by reliable existing fields.
6. Permit UNKNOWN / UNSURE.
7. Build a small real-world boundary set.
8. Measure which stage actually fails.
9. Only then decide whether the retriever, relation model,
   taxonomy, capability extraction, or decision policy needs work.

This also gives a cleaner answer when something fails.

Instead of:

“the semantic matcher got this wrong”

you can say:

the right concept never entered top-k

or:

retrieval was fine; the system collapsed a related concept into exact

or:

the canonical concept was fine, but a capability constraint was missing

or:

the source record itself was too vague to decide

Those point to very different fixes.

One thing I found especially useful in the real-world directory examples is that they make the normalization problem look less like “find the perfect canonical string” and more like “preserve enough distinctions that later stages can make the decision appropriate to their use case.”

So I think your original pipeline is still a good core:

raw label
    -> candidate concepts
    -> relation
    -> canonical service

I would just avoid treating canonical service as the end of the operational decision when capability matters.

For a discovery/search use case, a related or broader match may be perfectly useful.

For an actual provider-capability decision, the system may need one more layer:

canonical concept
    + provider/request context
    -> applicable / not applicable / conditional / unknown

That distinction seems to preserve the useful part of semantic normalization without asking normalization to erase exactly the real-world details that made the service distinguishable in the first place.

Yeah, I think the capability layer is the part that’s easy to overlook. A service can look like a good semantic match but still not be a valid result once you check what the provider actually handles.

I’ve found that keeping the original service wording and the provider’s actual restrictions separate makes the data much easier to work with. Otherwise, once everything gets normalized into one canonical label, some of the useful details can disappear.

I also like the idea of keeping unknown or unsure as a real outcome. In local services, there are plenty of cases where the provider simply doesn’t give enough information to make a confident decision. I’d rather have the system leave that for review than return the closest-looking service just because it has the highest embedding score.

The distinction between retrieval and the final decision is probably the biggest takeaway for me. Let the semantic layer find the possible matches, then use the actual business context to decide whether the match is usable. That also makes debugging much easier because you can see whether the problem came from retrieval, normalization, or missing provider information.

Your point about granularity is important too. A broad service label can be technically related without meaning that the provider handles every specific job underneath it. That’s probably where a lot of false positives can creep in if the system treats every related concept as an exact match.