Pattern 10: Semantic Suggestions
Part II: Interaction Patterns - Intelligence Patterns
Opening Scenario: The Address That Almost Wasn't
Jennifer was the IT director for a regional insurance company. Their agents used a web form to submit new policy applications, and one field caused constant problems: the property address.
Agents would type addresses from handwritten applications, and the variety was endless:
123 Main St
123 Main Street
123 Main St.
123 E Main Street
123 East Main St
One Twenty Three Main Street
All the same address, but entered six different ways. The database was a mess. Duplicate policies. Failed address verification. Misrouted mail.
Jennifer implemented basic autocomplete:
// Bad: Simple string matching
function getSuggestions(input) {
return addresses.filter(addr =>
addr.toLowerCase().startsWith(input.toLowerCase())
);
}
Agent types: "123 M" System suggests: - 123 Maple Ave - 123 Market St - 123 Main St
Better, but still primitive. The agent had to type the exact beginning of the street name to get suggestions.
One day, an agent named Tom complained: "I typed 'pittsburgh' and nothing happened. But when I typed 'pitt' it found Pittsburgh, Pennsylvania. Why doesn't it understand that 'pittsburgh' and 'pitt' mean the same thing?"
Jennifer realized the problem wasn't the technology - it was the semantics. The system was doing string matching, not understanding what the user meant.
Six months later, Jennifer demonstrated the new system. Tom typed: "123 main pitt"
The system suggested:
┌─────────────────────────────────────────────────────┐
│ 123 Main Street, Pittsburgh, PA 15217 │
│ 123 Main Street, Pittsburgh, PA 15213 │
│ 123 Mainway Drive, Pittsburgh, PA 15220 │
└─────────────────────────────────────────────────────┘
"How did it know I meant Pittsburgh?" Tom asked.
"It understands context," Jennifer explained. "It knows we're an insurance company in Pennsylvania. When you type 'pitt' or 'pittsburgh' in an address field, it semantically understands you probably mean Pittsburgh, PA - not Pittston, PA or Pittsfield, MA."
Tom typed: "penn"
The system suggested:
┌─────────────────────────────────────────────────────┐
│ Pennsylvania ← State (most likely) │
│ Penn Avenue │
│ Penn Hills │
│ Pennsbury Village │
└─────────────────────────────────────────────────────┘
"It knows that in a state field, 'penn' most likely means Pennsylvania. But in a street field, it might be Penn Avenue. It understands what each field represents."
Then Tom tried something tricky. He typed: "penna"
The system suggested:
┌─────────────────────────────────────────────────────┐
│ Pennsylvania ← Did you mean this? │
│ (Penna is an old abbreviation for Pennsylvania) │
└─────────────────────────────────────────────────────┘
"It even knows 'Penna' is an old abbreviation?" Tom was amazed.
"It understands variants," Jennifer said. "PA, Penn, Penna, Pennsylvania - all semantically equivalent in a state field."
The new system didn't just match patterns. It understood: - Abbreviations: PA = Pennsylvania, St = Street, Ave = Avenue - Alternatives: Main St = Main Street = Main - Misspellings: Pittsburg (no 'h') → Pittsburgh - Context: Same abbreviation means different things in different fields - Geography: Pittsburgh, PA more likely than Pittsburgh, CA for a PA-based company - Common patterns: If user types city first, suggest cities; if zip code, suggest complete addresses
Policy application errors dropped 47%. Duplicate addresses in the database fell by 68%. And agents could enter addresses twice as fast.
Context
Semantic Suggestions applies when:
User is entering structured data: Addresses, product names, categories, people, companies
Options are known: System has list of valid values (states, cities, products)
Variations exist: Same entity can be entered many ways (abbreviations, misspellings, alternatives)
Context provides clues: Field type, previous entries, user location inform likely values
Speed matters: Users benefit from faster entry
Accuracy is important: Suggestions reduce errors and inconsistencies
Learning is possible: System can improve from usage patterns
Problem Statement
Most autocomplete is primitive pattern matching with no semantic understanding:
Pure string matching:
// Bad: Only finds exact prefix matches
suggestions = options.filter(opt =>
opt.toLowerCase().startsWith(input.toLowerCase())
);
// User types "ny" → Finds "New York"
// User types "york" → Finds nothing!
// User types "new y" → Finds nothing!
No abbreviation understanding:
// Bad: Doesn't recognize abbreviations
// User types "PA" → No match for "Pennsylvania"
// User types "St" → No match for "Street"
// User types "Dr" → Suggests "Dr. Smith" not "Drake Avenue"
Context-blind suggestions:
// Bad: Same suggestions regardless of field type
// In state field: "CA" could be California or Canada
// In title field: "Dr" could be Doctor or Drive
// In company field: "PA" could be Pennsylvania or "PA Consulting"
No error tolerance:
// Bad: Typo breaks everything
// User types "Pennsilvania" → No suggestions
// User types "Californa" → No suggestions
// One wrong letter and you're on your own
No intelligence:
// Bad: No learning, no patterns
// User always selects "Pennsylvania" → Still has to type it fully
// User's location is Pittsburgh → Doesn't prioritize PA suggestions
// 90% of entries are in PA → Doesn't learn this pattern
We need suggestions that understand meaning, not just match strings.
Forces
Precision vs Recall
- Narrow suggestions are precise but miss valid options
- Broad suggestions include more but add noise
- Balance finding everything vs showing only best matches
Performance vs Intelligence
- Semantic understanding is computationally expensive
- Simple string matching is fast
- Balance speed with sophistication
Learning vs Privacy
- Better suggestions require tracking user behavior
- But users may not want behavior monitored
- Balance personalization with privacy
Explicit vs Implicit
- Show why suggestion was made?
- Or just show best suggestions silently?
- Transparency vs simplicity
Correction vs Acceptance
- Correct user's input automatically?
- Or just suggest corrections?
- Balance helpfulness with user control
Solution
Implement multi-layered suggestion engine that understands semantics through abbreviations, synonyms, fuzzy matching, context awareness, and learned patterns - presenting suggestions ranked by relevance with transparent reasoning.
The pattern has five suggestion strategies:
1. Semantic Matching (Understand Meaning)
Match based on meaning, not just characters:
class SemanticMatcher {
constructor() {
this.abbreviations = this.loadAbbreviations();
this.synonyms = this.loadSynonyms();
this.variants = this.loadVariants();
}
loadAbbreviations() {
return {
states: {
'PA': 'Pennsylvania',
'NY': 'New York',
'CA': 'California',
'Penn': 'Pennsylvania',
'Penna': 'Pennsylvania' // Historical abbreviation
},
streets: {
'St': 'Street',
'Ave': 'Avenue',
'Blvd': 'Boulevard',
'Dr': 'Drive',
'Ln': 'Lane',
'Ct': 'Court',
'Rd': 'Road',
'Pkwy': 'Parkway'
},
directions: {
'N': 'North',
'S': 'South',
'E': 'East',
'W': 'West',
'NE': 'Northeast',
'NW': 'Northwest',
'SE': 'Southeast',
'SW': 'Southwest'
},
titles: {
'Dr': 'Doctor',
'Mr': 'Mister',
'Mrs': 'Missus',
'Ms': 'Miss',
'Prof': 'Professor'
}
};
}
loadSynonyms() {
return {
'invoice': ['bill', 'statement', 'receipt'],
'customer': ['client', 'account', 'buyer'],
'urgent': ['asap', 'rush', 'priority', 'expedite'],
'cancel': ['void', 'delete', 'remove', 'abort']
};
}
match(input, candidates, context) {
const matches = [];
const normalized = this.normalize(input, context);
for (const candidate of candidates) {
const score = this.scoreMatch(normalized, candidate, context);
if (score > 0) {
matches.push({
value: candidate,
score: score,
matchType: this.getMatchType(normalized, candidate, context)
});
}
}
// Sort by score (highest first)
return matches.sort((a, b) => b.score - a.score);
}
normalize(input, context) {
let normalized = input.toLowerCase().trim();
// Expand abbreviations based on context
if (context.fieldType === 'state') {
const expansion = this.abbreviations.states[input.toUpperCase()];
if (expansion) {
normalized = expansion.toLowerCase();
}
} else if (context.fieldType === 'street') {
// Replace common street abbreviations
Object.entries(this.abbreviations.streets).forEach(([abbr, full]) => {
const pattern = new RegExp('\\b' + abbr + '\\b', 'gi');
normalized = normalized.replace(pattern, full);
});
}
return normalized;
}
scoreMatch(normalized, candidate, context) {
const candidateLower = candidate.toLowerCase();
let score = 0;
// Exact match
if (candidateLower === normalized) {
return 1000;
}
// Starts with (prefix match)
if (candidateLower.startsWith(normalized)) {
score += 100;
}
// Contains (substring match)
if (candidateLower.includes(normalized)) {
score += 50;
}
// Word boundary match (e.g., "main" matches "123 Main Street")
const words = candidateLower.split(/\s+/);
if (words.some(word => word.startsWith(normalized))) {
score += 75;
}
// Fuzzy match (allow typos)
const distance = this.levenshteinDistance(normalized, candidateLower);
const maxLength = Math.max(normalized.length, candidateLower.length);
const similarity = 1 - (distance / maxLength);
if (similarity > 0.7) { // 70% similar
score += similarity * 40;
}
// Boost if matches abbreviation
if (this.matchesAbbreviation(normalized, candidate, context)) {
score += 60;
}
// Boost if matches synonym
if (this.matchesSynonym(normalized, candidate)) {
score += 30;
}
return score;
}
levenshteinDistance(a, b) {
const matrix = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1, // substitution
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j] + 1 // deletion
);
}
}
}
return matrix[b.length][a.length];
}
matchesAbbreviation(input, candidate, context) {
const abbrs = this.abbreviations[context.fieldType];
if (!abbrs) return false;
return Object.entries(abbrs).some(([abbr, full]) =>
input.toUpperCase() === abbr && candidate.toLowerCase().includes(full.toLowerCase())
);
}
matchesSynonym(input, candidate) {
return Object.entries(this.synonyms).some(([word, syns]) =>
(input === word && syns.some(s => candidate.toLowerCase().includes(s))) ||
(syns.includes(input) && candidate.toLowerCase().includes(word))
);
}
}
2. Context-Aware Ranking
Prioritize suggestions based on context:
class ContextualRanker {
rank(matches, context) {
return matches.map(match => ({
...match,
contextScore: this.getContextScore(match, context),
finalScore: match.score + this.getContextScore(match, context)
}))
.sort((a, b) => b.finalScore - a.finalScore);
}
getContextScore(match, context) {
let score = 0;
// Geographic proximity
if (context.userLocation && match.location) {
const distance = this.getDistance(context.userLocation, match.location);
// Closer locations score higher
if (distance < 10) score += 50; // Within 10 miles
else if (distance < 50) score += 30; // Within 50 miles
else if (distance < 200) score += 10; // Within 200 miles
}
// Historical usage
if (context.userHistory) {
const usageCount = context.userHistory.filter(h =>
h.value === match.value
).length;
score += Math.min(usageCount * 5, 50); // Cap at 50 points
}
// Organizational patterns
if (context.organizationStats) {
const orgUsage = context.organizationStats[match.value] || 0;
score += Math.min(orgUsage * 2, 30); // Cap at 30 points
}
// Temporal relevance
if (match.temporal) {
if (this.isCurrentlyRelevant(match.temporal, context.currentDate)) {
score += 20;
}
}
// Domain-specific boosting
if (context.fieldType === 'state' && context.companyState) {
// Boost suggestions in same state as company
if (match.value === context.companyState) {
score += 40;
}
}
if (context.fieldType === 'category') {
// Boost categories user has used before
if (context.userCategories.includes(match.value)) {
score += 35;
}
}
return score;
}
getDistance(loc1, loc2) {
// Haversine formula for geographic distance
const R = 3959; // Earth's radius in miles
const dLat = this.toRad(loc2.lat - loc1.lat);
const dLon = this.toRad(loc2.lon - loc1.lon);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(this.toRad(loc1.lat)) * Math.cos(this.toRad(loc2.lat)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
toRad(degrees) {
return degrees * (Math.PI / 180);
}
isCurrentlyRelevant(temporal, currentDate) {
if (temporal.seasonal) {
const month = currentDate.getMonth();
return temporal.relevantMonths.includes(month);
}
if (temporal.timeOfDay) {
const hour = currentDate.getHours();
return hour >= temporal.startHour && hour <= temporal.endHour;
}
return true;
}
}
3. Multi-Field Intelligence
Suggest based on relationships between fields:
class MultiFieldSuggester {
suggest(currentField, currentValue, formState, context) {
const suggestions = [];
// If user entered city, suggest matching state
if (currentField === 'state' && formState.city) {
const statesForCity = this.getStatesForCity(formState.city);
if (statesForCity.length === 1) {
// Only one state has this city - auto-suggest it
suggestions.push({
value: statesForCity[0],
reason: `Based on ${formState.city}`,
confidence: 0.95
});
} else if (statesForCity.length > 1) {
// Multiple states have this city - suggest all with context
statesForCity.forEach(state => {
suggestions.push({
value: state,
reason: `${formState.city} exists in ${state}`,
confidence: 0.7
});
});
}
}
// If user entered zip code, suggest city and state
if (currentField === 'city' && formState.zipCode) {
const location = this.getLocationForZip(formState.zipCode);
if (location) {
suggestions.push({
value: location.city,
additionalFields: {
state: location.state
},
reason: `Based on ZIP ${formState.zipCode}`,
confidence: 0.99
});
}
}
// If user selected customer, suggest their default values
if (currentField === 'paymentTerms' && formState.customer) {
const customer = this.getCustomer(formState.customer);
suggestions.push({
value: customer.defaultPaymentTerms,
reason: `${customer.name}'s usual terms`,
confidence: 0.85
});
}
// If user selected product category, suggest relevant products
if (currentField === 'product' && formState.category) {
const products = this.getProductsInCategory(formState.category);
products.forEach(product => {
suggestions.push({
value: product.name,
metadata: {
price: product.price,
sku: product.sku
},
reason: `In ${formState.category} category`,
confidence: 0.8
});
});
}
return this.rankByConfidence(suggestions);
}
getStatesForCity(city) {
// Some cities exist in multiple states
const cityMap = {
'Springfield': ['IL', 'MA', 'MO', 'OH'],
'Portland': ['OR', 'ME'],
'Columbus': ['OH', 'GA', 'IN'],
'Pittsburgh': ['PA'] // Unique
};
return cityMap[city] || [];
}
getLocationForZip(zipCode) {
// ZIP code uniquely identifies city/state
const zipMap = {
'15217': { city: 'Pittsburgh', state: 'PA' },
'10001': { city: 'New York', state: 'NY' },
'90210': { city: 'Beverly Hills', state: 'CA' }
};
return zipMap[zipCode];
}
}
4. Learning Engine
Improve suggestions based on user behavior:
class LearningSuggester {
constructor() {
this.userPatterns = new Map();
this.organizationPatterns = new Map();
}
recordSelection(userId, field, input, selectedValue, context) {
// Track what user typed and what they selected
const pattern = {
userId,
field,
input,
selected: selectedValue,
timestamp: new Date(),
context: {
timeOfDay: new Date().getHours(),
dayOfWeek: new Date().getDay(),
relatedFields: context.formState
}
};
this.storePattern(pattern);
this.updateUserPatterns(userId, field, input, selectedValue);
this.updateOrganizationPatterns(field, input, selectedValue);
}
updateUserPatterns(userId, field, input, selectedValue) {
if (!this.userPatterns.has(userId)) {
this.userPatterns.set(userId, new Map());
}
const userFieldPatterns = this.userPatterns.get(userId);
const key = `${field}:${input}`;
if (!userFieldPatterns.has(key)) {
userFieldPatterns.set(key, {
selections: [],
frequency: new Map()
});
}
const pattern = userFieldPatterns.get(key);
pattern.selections.push({
value: selectedValue,
timestamp: new Date()
});
// Update frequency
const count = pattern.frequency.get(selectedValue) || 0;
pattern.frequency.set(selectedValue, count + 1);
// Keep only last 100 selections
if (pattern.selections.length > 100) {
pattern.selections = pattern.selections.slice(-100);
}
}
updateOrganizationPatterns(field, input, selectedValue) {
const key = `${field}:${input}`;
if (!this.organizationPatterns.has(key)) {
this.organizationPatterns.set(key, {
totalSelections: 0,
valueFrequency: new Map()
});
}
const pattern = this.organizationPatterns.get(key);
pattern.totalSelections++;
const count = pattern.valueFrequency.get(selectedValue) || 0;
pattern.valueFrequency.set(selectedValue, count + 1);
}
getLearnedSuggestions(userId, field, input, context) {
const suggestions = [];
// Personal patterns (highest priority)
const userPattern = this.getUserPattern(userId, field, input);
if (userPattern) {
// Sort by frequency
const sorted = Array.from(userPattern.frequency.entries())
.sort((a, b) => b[1] - a[1]);
sorted.forEach(([value, count]) => {
suggestions.push({
value,
score: count * 10, // Weight by frequency
source: 'personal',
reason: `You selected this ${count} times before`
});
});
}
// Organization patterns (lower priority)
const orgPattern = this.getOrganizationPattern(field, input);
if (orgPattern) {
const sorted = Array.from(orgPattern.valueFrequency.entries())
.sort((a, b) => b[1] - a[1]);
sorted.slice(0, 5).forEach(([value, count]) => {
const percentage = (count / orgPattern.totalSelections * 100).toFixed(0);
suggestions.push({
value,
score: count * 2, // Lower weight than personal
source: 'organization',
reason: `${percentage}% of users select this`
});
});
}
return suggestions;
}
getUserPattern(userId, field, input) {
const userFieldPatterns = this.userPatterns.get(userId);
if (!userFieldPatterns) return null;
const key = `${field}:${input}`;
return userFieldPatterns.get(key);
}
getOrganizationPattern(field, input) {
const key = `${field}:${input}`;
return this.organizationPatterns.get(key);
}
}
5. Intelligent Presentation
Display suggestions with clarity and context:
class SuggestionPresenter {
present(suggestions, input, context) {
// Group and format suggestions
const grouped = this.groupSuggestions(suggestions);
const formatted = this.formatSuggestions(grouped, input, context);
return {
suggestions: formatted,
metadata: {
totalFound: suggestions.length,
searchTerm: input,
searchStrategy: this.getStrategyDescription(suggestions)
}
};
}
groupSuggestions(suggestions) {
const groups = {
exact: [],
personal: [],
popular: [],
fuzzy: [],
related: []
};
suggestions.forEach(sug => {
if (sug.matchType === 'exact') {
groups.exact.push(sug);
} else if (sug.source === 'personal') {
groups.personal.push(sug);
} else if (sug.source === 'organization') {
groups.popular.push(sug);
} else if (sug.matchType === 'fuzzy') {
groups.fuzzy.push(sug);
} else {
groups.related.push(sug);
}
});
return groups;
}
formatSuggestions(grouped, input, context) {
const formatted = [];
// Exact matches first (if any)
if (grouped.exact.length > 0) {
formatted.push({
section: 'Exact matches',
items: grouped.exact.map(s => this.formatItem(s, input))
});
}
// Personal history
if (grouped.personal.length > 0) {
formatted.push({
section: 'You\'ve used',
items: grouped.personal.slice(0, 3).map(s => this.formatItem(s, input))
});
}
// Popular/common
if (grouped.popular.length > 0) {
formatted.push({
section: 'Commonly used',
items: grouped.popular.slice(0, 5).map(s => this.formatItem(s, input))
});
}
// Fuzzy/corrected
if (grouped.fuzzy.length > 0 && grouped.exact.length === 0) {
formatted.push({
section: 'Did you mean?',
items: grouped.fuzzy.slice(0, 3).map(s => this.formatItem(s, input))
});
}
// Related
if (grouped.related.length > 0) {
formatted.push({
section: 'Related',
items: grouped.related.slice(0, 3).map(s => this.formatItem(s, input))
});
}
return formatted;
}
formatItem(suggestion, input) {
return {
value: suggestion.value,
display: this.highlightMatch(suggestion.value, input),
secondary: suggestion.reason || suggestion.metadata,
icon: this.getIcon(suggestion),
confidence: suggestion.confidence
};
}
highlightMatch(text, input) {
// Highlight the matched portion
const index = text.toLowerCase().indexOf(input.toLowerCase());
if (index === -1) return text;
return {
before: text.substring(0, index),
match: text.substring(index, index + input.length),
after: text.substring(index + input.length)
};
}
getIcon(suggestion) {
if (suggestion.source === 'personal') return '⭐';
if (suggestion.source === 'organization') return '👥';
if (suggestion.matchType === 'fuzzy') return '🔍';
if (suggestion.confidence > 0.9) return '✓';
return null;
}
getStrategyDescription(suggestions) {
const strategies = new Set();
suggestions.forEach(s => {
if (s.matchType) strategies.add(s.matchType);
if (s.source) strategies.add(s.source);
});
return Array.from(strategies).join(', ');
}
}
Implementation Details
Complete Semantic Suggestion Engine
class SemanticSuggestionEngine {
constructor(config) {
this.matcher = new SemanticMatcher();
this.ranker = new ContextualRanker();
this.multiField = new MultiFieldSuggester();
this.learner = new LearningSuggester();
this.presenter = new SuggestionPresenter();
this.config = config;
}
async getSuggestions(field, input, context) {
if (!input || input.length < this.config.minInputLength) {
return this.getDefaultSuggestions(field, context);
}
// Get candidates from all sources
const candidates = await this.getCandidates(field, context);
// Semantic matching
const semanticMatches = this.matcher.match(input, candidates, {
fieldType: field.type,
...context
});
// Multi-field intelligence
const multiFieldSuggestions = this.multiField.suggest(
field.name,
input,
context.formState,
context
);
// Learned patterns
const learnedSuggestions = this.learner.getLearnedSuggestions(
context.userId,
field.name,
input,
context
);
// Combine all suggestions
const allSuggestions = [
...semanticMatches,
...multiFieldSuggestions,
...learnedSuggestions
];
// Remove duplicates
const unique = this.deduplicateSuggestions(allSuggestions);
// Context-aware ranking
const ranked = this.ranker.rank(unique, context);
// Present to user
return this.presenter.present(ranked.slice(0, 10), input, context);
}
async getCandidates(field, context) {
// Get possible values from various sources
const candidates = [];
// Static options (if field has predefined list)
if (field.options) {
candidates.push(...field.options);
}
// Database lookup
if (field.dataSource) {
const dbResults = await this.queryDataSource(field.dataSource, context);
candidates.push(...dbResults);
}
// External API
if (field.externalSource) {
const apiResults = await this.queryExternalSource(field.externalSource, context);
candidates.push(...apiResults);
}
return candidates;
}
deduplicateSuggestions(suggestions) {
const seen = new Map();
suggestions.forEach(sug => {
const existing = seen.get(sug.value);
if (!existing || sug.score > existing.score) {
seen.set(sug.value, sug);
}
});
return Array.from(seen.values());
}
getDefaultSuggestions(field, context) {
// When no input, show most relevant suggestions
const defaults = [];
// User's most recent selections
const recent = this.learner.getRecentSelections(context.userId, field.name, 5);
defaults.push(...recent.map(r => ({
value: r,
source: 'recent',
reason: 'Recently used'
})));
// Most popular in organization
const popular = this.learner.getMostPopular(field.name, 5);
defaults.push(...popular.map(p => ({
value: p.value,
source: 'popular',
reason: `Used by ${p.percentage}% of users`
})));
return this.presenter.present(defaults, '', context);
}
recordSelection(field, input, selectedValue, context) {
// Learn from user's selection
this.learner.recordSelection(
context.userId,
field.name,
input,
selectedValue,
context
);
}
}
// Usage
const engine = new SemanticSuggestionEngine({
minInputLength: 1
});
// User types in state field
const suggestions = await engine.getSuggestions(
{ name: 'state', type: 'state', options: US_STATES },
'penn',
{
userId: 'user123',
userLocation: { lat: 40.4406, lon: -79.9959 }, // Pittsburgh
companyState: 'PA',
formState: {
city: 'Pittsburgh'
}
}
);
// Returns ranked suggestions:
// 1. Pennsylvania (exact abbreviation match + company state + user location)
// 2. Penn Hills (contains "penn")
// 3. Pennsbury (contains "penn")
Consequences
Benefits
Faster Data Entry: - Users type less - Quick selection from suggestions - Reduced cognitive load
Fewer Errors: - Consistent formatting - Correct spellings - Valid values only
Better User Experience: - Feels intelligent and helpful - Anticipates user needs - Reduces frustration
Data Quality: - Standardized entries - No duplicates from variations - Clean, consistent database
Learning System: - Improves over time - Adapts to user patterns - Organization learns collectively
Liabilities
Implementation Complexity: - Multiple matching strategies - Context gathering - Performance optimization needed
Performance Concerns: - Semantic matching is expensive - External lookups add latency - Need caching and optimization
Privacy Considerations: - Learning tracks user behavior - May reveal sensitive patterns - Need clear privacy policies
Overwhelming Users: - Too many suggestions confuse - Need smart ranking and limiting - Balance helpfulness with simplicity
Maintenance Burden: - Abbreviation lists need updates - Synonym maps grow over time - Learning patterns need pruning
Domain Examples
Healthcare: Medication Names
const medicationSuggester = {
// Understand brand vs generic names
synonyms: {
'acetaminophen': ['tylenol', 'paracetamol'],
'ibuprofen': ['advil', 'motrin'],
'lisinopril': ['zestril', 'prinivil']
},
// Correct common misspellings
fuzzyMatch: {
'azythromycin': 'azithromycin',
'amiodorone': 'amiodarone',
'metropolol': 'metoprolol'
},
// Suggest based on diagnosis
contextual: {
diagnosis: 'hypertension',
suggestFirst: ['lisinopril', 'amlodipine', 'metoprolol']
}
};
Legal: Case Citations
const citationSuggester = {
// Understand citation formats
patterns: {
'Brown v Board': '347 U.S. 483 (1954)',
'Roe v Wade': '410 U.S. 113 (1973)',
'Miranda': 'Miranda v. Arizona, 384 U.S. 436 (1966)'
},
// Suggest recent cases in same area
contextual: (area, jurisdiction) => {
if (area === 'contract' && jurisdiction === 'PA') {
return recentContractCasesPA();
}
},
// Learn attorney's commonly cited cases
personal: (attorneyId) => {
return getFrequentCitations(attorneyId);
}
};
E-commerce: Product Search
const productSuggester = {
// Understand product attributes
semantic: {
'red shirt': { category: 'clothing', color: 'red', type: 'shirt' },
'mens large': { gender: 'mens', size: 'large' },
'wireless headphones': { category: 'electronics', features: ['wireless'] }
},
// Suggest based on browsing history
personal: (userId) => {
const history = getUserBrowsingHistory(userId);
const categories = history.map(h => h.category);
return suggestInCategories(categories);
},
// Seasonal suggestions
temporal: (date) => {
const month = date.getMonth();
if (month === 11) return ['gifts', 'holiday', 'winter'];
if (month === 7) return ['back to school', 'summer'];
}
};
Related Patterns
Prerequisites: - Volume 3, Pattern 8: Intelligent Defaults (suggestions as defaults)
Synergies: - Volume 3, Pattern 6: Domain-Aware Validation (suggestions must be valid) - Volume 3, Pattern 9: Contextual Constraints (suggestions respect constraints) - Volume 3, Pattern 11: Cascading Updates (selecting suggestion updates related fields)
Conflicts: - Offline forms (can't query external sources) - Privacy-focused systems (can't learn from behavior)
Alternatives: - Dropdowns (when options are few and stable) - Radio buttons (when options are mutually exclusive) - Free text (when variety is too great to suggest)
Known Uses
Google Search: Autocomplete understands typos, synonyms, and trending queries
Amazon Product Search: Suggests based on browsing history, popularity, and current searches
Address Autocomplete (Google Places, SmartyStreets): Geographic intelligence with abbreviation understanding
Email Autocomplete (Gmail, Outlook): Learns from sent mail and contact frequency
Code Editors (VS Code, IntelliJ): Context-aware code completion based on types and imports
Medical Records (Epic, Cerner): Medication autocomplete with generic/brand name matching
CRM Systems (Salesforce, HubSpot): Contact and company suggestions from database and external sources
Further Reading
Academic Foundations
- Information Retrieval: Manning, C.D., Raghavan, P., & Schütze, H. (2008). Introduction to Information Retrieval. Cambridge University Press. https://nlp.stanford.edu/IR-book/ - Free online textbook
- Autocomplete Algorithms: Bar-Yossef, Z., & Kraus, N. (2011). "Context-Sensitive Query Auto-Completion." WWW '11. https://dl.acm.org/doi/10.1145/1963405.1963506
- Fuzzy String Matching: Navarro, G. (2001). "A Guided Tour to Approximate String Matching." ACM Computing Surveys 33(1): 31-88.
Practical Implementation
- Fuse.js: https://fusejs.io/ - Lightweight fuzzy-search library for JavaScript
- Algolia Autocomplete: https://www.algolia.com/doc/ui-libraries/autocomplete/introduction/what-is-autocomplete/ - Production-grade autocomplete
- React Autosuggest: https://github.com/moroshko/react-autosuggest - Accessible autocomplete component
- Downshift: https://www.downshift-js.com/ - Flexible autocomplete/combobox primitives
- ElasticSearch Suggesters: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html - Server-side suggestions
Standards & Accessibility
- ARIA Combobox Pattern: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/ - Accessible autocomplete implementation
- HTML datalist: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist - Native browser autocomplete
Related Trilogy Patterns
- Pattern 6: Domain-Aware Validation - Validating suggested values
- Pattern 14: Cross-Field Validation - Validate selected suggestions
- Pattern 22: Realtime Lookup - External data for suggestions
- Volume 2, Pattern 11: Historical Pattern Matching - Learning from past user selections
- Volume 2, Pattern 26: Feedback Loop Implementation - Improving suggestions over time
- Volume 1, Chapter 8: Architecture of Domain-Specific Systems - Autocomplete architecture
- Volume 1, Chapter 10: Domain Knowledge Acquisition - Building suggestion databases
Tools & Services
- Google Places Autocomplete API: https://developers.google.com/maps/documentation/places/web-service/autocomplete - Geographic suggestions
- SmartyStreets: https://www.smartystreets.com/products/us-address-autocomplete - Address validation
- Typesense: https://typesense.org/ - Fast typo-tolerant search engine
- MeiliSearch: https://www.meilisearch.com/ - Instant search with typo tolerance
Implementation Examples
- Autocomplete Best Practices: https://www.nngroup.com/articles/autocomplete/ - Nielsen Norman Group guidelines
- Building Accessible Autocomplete: https://technology.blog.gov.uk/2020/10/06/building-accessible-autocomplete/ - UK Government Digital Service