patent-query-shortcuts
v0.2.2
Published
Shortcut expansion + template system for patent search queries (e.g., TAC -> Title/Abstract/Claims)
Readme
patent-query-shortcuts
Shortcut expansion + template system for patent search queries. Type TAC and get TAC_ALL:(Title, Abstract, Claims) with smart autocomplete, date range snippets, and usage-based ranking.
Install
npm i patent-query-shortcutsTable of Contents
Quick Start
import { createRegistry, patsnapTemplate, nuvoaiTemplate } from "patent-query-shortcuts";
// Step 1: Create a registry with templates
const reg = createRegistry([patsnapTemplate, nuvoaiTemplate]);
// Step 2: Get autocomplete suggestions (for frontend)
const result = reg.getCompletions("TA", 2, { templateId: "patsnap" });
// Returns: { token: { start: 0, end: 2, prefix: "TA" }, items: [...] }
// Step 3: Expand shortcuts in a query (for backend)
const expanded = reg.expand(`TAC:(stent) AND APD:[20260101 TO 20261231]`, {
templateId: "patsnap"
});
// => TAC_ALL:(stent) AND APD:[20260101 TO 20261231]Frontend Usage
Use this package in your frontend to provide autocomplete suggestions as users type patent query shortcuts.
React Example
Step-by-step implementation:
- Install and import the package:
import { createRegistry, patsnapTemplate } from "patent-query-shortcuts";
import { useState, useMemo, useCallback } from "react";- Create a registry instance (do this once, outside component or in useMemo):
function PatentQueryInput() {
// Create registry once - this is expensive, so use useMemo
const registry = useMemo(() => {
return createRegistry([patsnapTemplate]);
}, []);
const [query, setQuery] = useState("");
const [cursorPosition, setCursorPosition] = useState(0);
const [suggestions, setSuggestions] = useState([]);
const [selectedIndex, setSelectedIndex] = useState(0);- Handle text input and get completions:
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
const text = e.target.value;
const cursor = e.target.selectionStart;
setQuery(text);
setCursorPosition(cursor);
// Get completions for current cursor position
const result = registry.getCompletions(text, cursor, {
templateId: "patsnap",
limit: 10,
trailingSpace: true, // Add space after completion
allowEmptyPrefix: false // Only show suggestions when typing
});
setSuggestions(result.items);
setSelectedIndex(0);
}, [registry]);- Handle keyboard navigation (Arrow keys, Enter, Tab):
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (suggestions.length === 0) return;
if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedIndex(prev => (prev + 1) % suggestions.length);
} else if (e.key === "ArrowUp") {
e.preventDefault();
setSelectedIndex(prev => (prev - 1 + suggestions.length) % suggestions.length);
} else if (e.key === "Enter" || e.key === "Tab") {
e.preventDefault();
applySuggestion(suggestions[selectedIndex]);
}
}, [suggestions, selectedIndex]);- Apply selected suggestion:
const applySuggestion = useCallback((item: CompletionItem) => {
// Apply the completion
const result = registry.applyCompletion(query, item);
setQuery(result.text);
setCursorPosition(result.cursor);
setSuggestions([]);
// Record usage for better ranking
registry.recordUsage("patsnap", item.key, 1);
// Focus back to textarea and set cursor position
setTimeout(() => {
const textarea = document.querySelector("textarea");
if (textarea) {
textarea.focus();
textarea.setSelectionRange(result.cursor, result.cursor);
}
}, 0);
}, [query, registry]);- Render the UI:
return (
<div className="query-input-container">
<textarea
value={query}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Type TAC, APD, etc. for suggestions..."
rows={5}
style={{ width: "100%", padding: "8px" }}
/>
{/* Suggestions dropdown */}
{suggestions.length > 0 && (
<div className="suggestions-dropdown">
{suggestions.map((item, idx) => (
<div
key={item.key}
className={`suggestion-item ${idx === selectedIndex ? "selected" : ""}`}
onClick={() => applySuggestion(item)}
onMouseEnter={() => setSelectedIndex(idx)}
>
<strong>{item.label}</strong>
{item.description && (
<span className="suggestion-desc">{item.description}</span>
)}
</div>
))}
</div>
)}
</div>
);
}Complete React Component:
import { createRegistry, patsnapTemplate, type CompletionItem } from "patent-query-shortcuts";
import { useState, useMemo, useCallback } from "react";
function PatentQueryInput() {
const registry = useMemo(() => createRegistry([patsnapTemplate]), []);
const [query, setQuery] = useState("");
const [cursorPosition, setCursorPosition] = useState(0);
const [suggestions, setSuggestions] = useState<CompletionItem[]>([]);
const [selectedIndex, setSelectedIndex] = useState(0);
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
const text = e.target.value;
const cursor = e.target.selectionStart;
setQuery(text);
setCursorPosition(cursor);
const result = registry.getCompletions(text, cursor, {
templateId: "patsnap",
limit: 10,
trailingSpace: true
});
setSuggestions(result.items);
setSelectedIndex(0);
}, [registry]);
const applySuggestion = useCallback((item: CompletionItem) => {
const result = registry.applyCompletion(query, item);
setQuery(result.text);
setCursorPosition(result.cursor);
setSuggestions([]);
registry.recordUsage("patsnap", item.key, 1);
}, [query, registry]);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (suggestions.length === 0) return;
if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedIndex(prev => (prev + 1) % suggestions.length);
} else if (e.key === "ArrowUp") {
e.preventDefault();
setSelectedIndex(prev => (prev - 1 + suggestions.length) % suggestions.length);
} else if (e.key === "Enter" || e.key === "Tab") {
e.preventDefault();
applySuggestion(suggestions[selectedIndex]);
}
}, [suggestions, selectedIndex, applySuggestion]);
return (
<div className="query-input-container">
<textarea
value={query}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Type TAC, APD, etc. for suggestions..."
rows={5}
/>
{suggestions.length > 0 && (
<div className="suggestions-dropdown">
{suggestions.map((item, idx) => (
<div
key={item.key}
className={idx === selectedIndex ? "selected" : ""}
onClick={() => applySuggestion(item)}
>
<strong>{item.label}</strong>
{item.description && <span>{item.description}</span>}
</div>
))}
</div>
)}
</div>
);
}Vue.js Example
Step-by-step implementation:
- Install and setup:
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { createRegistry, patsnapTemplate, type CompletionItem } from 'patent-query-shortcuts';
// Step 1: Create registry (do this once)
const registry = createRegistry([patsnapTemplate]);
// Step 2: Reactive state
const query = ref('');
const cursorPosition = ref(0);
const suggestions = ref<CompletionItem[]>([]);
const selectedIndex = ref(0);- Handle input changes:
const handleInput = (e: Event) => {
const target = e.target as HTMLTextAreaElement;
query.value = target.value;
cursorPosition.value = target.selectionStart;
// Step 3: Get completions
const result = registry.getCompletions(query.value, cursorPosition.value, {
templateId: 'patsnap',
limit: 10,
trailingSpace: true
});
suggestions.value = result.items;
selectedIndex.value = 0;
};- Apply suggestion:
const applySuggestion = (item: CompletionItem) => {
const result = registry.applyCompletion(query.value, item);
query.value = result.text;
cursorPosition.value = result.cursor;
suggestions.value = [];
registry.recordUsage('patsnap', item.key, 1);
// Update textarea cursor
nextTick(() => {
const textarea = document.querySelector('textarea');
if (textarea) {
textarea.setSelectionRange(result.cursor, result.cursor);
}
});
};- Template:
<template>
<div class="query-input-container">
<textarea
v-model="query"
@input="handleInput"
@keydown="handleKeyDown"
placeholder="Type TAC, APD, etc..."
rows="5"
/>
<div v-if="suggestions.length > 0" class="suggestions">
<div
v-for="(item, idx) in suggestions"
:key="item.key"
:class="{ selected: idx === selectedIndex }"
@click="applySuggestion(item)"
>
<strong>{{ item.label }}</strong>
<span v-if="item.description">{{ item.description }}</span>
</div>
</div>
</div>
</template>Vanilla JavaScript Example
Step-by-step implementation:
<!DOCTYPE html>
<html>
<head>
<title>Patent Query Input</title>
</head>
<body>
<textarea id="queryInput" rows="5" style="width: 100%;"></textarea>
<div id="suggestions"></div>
<script type="module">
// Step 1: Import the package
import { createRegistry, patsnapTemplate } from './node_modules/patent-query-shortcuts/dist/index.js';
// Step 2: Create registry
const registry = createRegistry([patsnapTemplate]);
// Step 3: Get DOM elements
const textarea = document.getElementById('queryInput');
const suggestionsDiv = document.getElementById('suggestions');
let selectedIndex = 0;
let currentSuggestions = [];
// Step 4: Handle input
textarea.addEventListener('input', (e) => {
const text = e.target.value;
const cursor = e.target.selectionStart;
// Get completions
const result = registry.getCompletions(text, cursor, {
templateId: 'patsnap',
limit: 10,
trailingSpace: true
});
currentSuggestions = result.items;
selectedIndex = 0;
renderSuggestions();
});
// Step 5: Handle keyboard
textarea.addEventListener('keydown', (e) => {
if (currentSuggestions.length === 0) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = (selectedIndex + 1) % currentSuggestions.length;
renderSuggestions();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = (selectedIndex - 1 + currentSuggestions.length) % currentSuggestions.length;
renderSuggestions();
} else if (e.key === 'Enter' || e.key === 'Tab') {
e.preventDefault();
applySuggestion(currentSuggestions[selectedIndex]);
}
});
// Step 6: Apply suggestion
function applySuggestion(item) {
const result = registry.applyCompletion(textarea.value, item);
textarea.value = result.text;
suggestionsDiv.innerHTML = '';
currentSuggestions = [];
registry.recordUsage('patsnap', item.key, 1);
// Set cursor position
setTimeout(() => {
textarea.setSelectionRange(result.cursor, result.cursor);
textarea.focus();
}, 0);
}
// Step 7: Render suggestions
function renderSuggestions() {
if (currentSuggestions.length === 0) {
suggestionsDiv.innerHTML = '';
return;
}
suggestionsDiv.innerHTML = currentSuggestions.map((item, idx) => `
<div class="suggestion ${idx === selectedIndex ? 'selected' : ''}"
onclick="applySuggestion(${JSON.stringify(item).replace(/"/g, '"')})">
<strong>${item.label}</strong>
${item.description ? `<span>${item.description}</span>` : ''}
</div>
`).join('');
}
</script>
</body>
</html>Backend Usage
Use this package in your backend to expand user queries before sending them to your patent search API.
Express.js API Example
Step-by-step implementation:
- Install and create service:
// services/queryService.ts
import { createRegistry, patsnapTemplate, nuvoaiTemplate } from "patent-query-shortcuts";
// Step 1: Create registry with all templates
const registry = createRegistry([patsnapTemplate, nuvoaiTemplate]);
// Step 2: Export query processing functions
export class QueryService {
/**
* Expand shortcuts in a user query
* @param query - User's query with shortcuts (e.g., "TAC:(stent)")
* @param templateId - Which template to use (e.g., "patsnap")
* @returns Expanded query ready for search API
*/
static expandQuery(query: string, templateId: string): string {
return registry.expand(query, {
templateId,
wrapBareValues: true // Wrap bare values in parentheses
});
}
/**
* Validate and expand query
* @param query - User query
* @param templateId - Template ID
* @returns Object with expanded query and validation info
*/
static processQuery(query: string, templateId: string) {
const expanded = this.expandQuery(query, templateId);
return {
original: query,
expanded: expanded,
templateId: templateId,
isValid: expanded.length > 0,
timestamp: new Date().toISOString()
};
}
/**
* Get available templates
*/
static getTemplates() {
return registry.listTemplates();
}
}- Create Express routes:
// routes/queryRoutes.ts
import express from "express";
import { QueryService } from "../services/queryService.js";
const router = express.Router();
// Step 3: POST endpoint to expand query
router.post("/expand", (req, res) => {
try {
const { query, templateId = "patsnap" } = req.body;
if (!query || typeof query !== "string") {
return res.status(400).json({
error: "Query is required and must be a string"
});
}
// Step 4: Process and expand the query
const result = QueryService.processQuery(query, templateId);
res.json({
success: true,
data: result
});
} catch (error) {
res.status(500).json({
error: "Failed to process query",
message: error.message
});
}
});
// Step 5: GET endpoint for available templates
router.get("/templates", (req, res) => {
try {
const templates = QueryService.getTemplates();
res.json({
success: true,
data: templates
});
} catch (error) {
res.status(500).json({
error: "Failed to get templates"
});
}
});
export default router;- Use in your Express app:
// app.ts
import express from "express";
import queryRoutes from "./routes/queryRoutes.js";
const app = express();
app.use(express.json());
app.use("/api/query", queryRoutes);
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});- Example API usage:
# Expand a query
curl -X POST http://localhost:3000/api/query/expand \
-H "Content-Type: application/json" \
-d '{
"query": "TAC:(stent) AND APD:[20260101 TO 20261231]",
"templateId": "patsnap"
}'
# Response:
{
"success": true,
"data": {
"original": "TAC:(stent) AND APD:[20260101 TO 20261231]",
"expanded": "TAC_ALL:(stent) AND APD:[20260101 TO 20261231]",
"templateId": "patsnap",
"isValid": true,
"timestamp": "2024-01-15T10:30:00.000Z"
}
}Query Processing Service
Complete backend service example:
// services/patentSearchService.ts
import { createRegistry, patsnapTemplate } from "patent-query-shortcuts";
const registry = createRegistry([patsnapTemplate]);
export class PatentSearchService {
/**
* Step 1: Preprocess user query
* - Expand shortcuts
* - Validate syntax
* - Log for analytics
*/
static preprocessQuery(userQuery: string, templateId: string = "patsnap") {
// Expand shortcuts
const expandedQuery = registry.expand(userQuery, {
templateId,
wrapBareValues: true
});
return {
original: userQuery,
expanded: expandedQuery,
templateId
};
}
/**
* Step 2: Execute search with expanded query
*/
static async searchPatents(query: string, templateId: string = "patsnap") {
// Preprocess
const processed = this.preprocessQuery(query, templateId);
// Step 3: Send to your patent search API
const response = await fetch("https://your-patent-api.com/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: processed.expanded,
template: templateId
})
});
const results = await response.json();
// Step 4: Return results with metadata
return {
results,
query: processed.expanded,
originalQuery: processed.original,
templateId: processed.templateId
};
}
/**
* Step 5: Track which shortcuts users use (for analytics)
*/
static trackShortcutUsage(templateId: string, shortcutKey: string) {
registry.recordUsage(templateId, shortcutKey, 1);
// Optionally save to database for analytics
console.log(`Tracked usage: ${templateId}/${shortcutKey}`);
}
}
// Usage in your API endpoint:
app.post("/api/search", async (req, res) => {
const { query, templateId } = req.body;
try {
const results = await PatentSearchService.searchPatents(query, templateId);
res.json(results);
} catch (error) {
res.status(500).json({ error: error.message });
}
});Features
✅ Smart Autocomplete
- Prefix matching: Type
TAto seeTA,TAC,TACDsuggestions - Smart ranking: Exact match → Most-used → Shorter key → Alphabetical
- Cursor positioning: Snippets place cursor at the right spot (e.g.,
TAC:(|))
✅ Date/Range Snippets
Date fields automatically insert range syntax:
APD:[| TO ]→ Application Date rangeB_CITES_COUNT:[| TO *]→ Citation count range
✅ Trailing Space Option
Insert space after completion while keeping cursor inside brackets:
const items = reg.complete("TAC", 3, {
templateId: "patsnap",
trailingSpace: true
});
// Inserts: "TAC:() " with cursor still inside ()✅ Usage Tracking
Record which shortcuts users select to improve ranking:
reg.recordUsage("patsnap", "TAC", 1);
// TAC will now rank higher in future suggestions✅ JSON Template Loader
Load templates from JSON:
import { loadTemplateFromJson, loadTemplatesFromJson } from "patent-query-shortcuts";
const template = loadTemplateFromJson({
id: "custom",
name: "Custom Template",
shortcuts: [
{ key: "TEST", kind: "field", field: "TEST_FIELD", snippet: "TEST:(|)" }
]
});✅ Built-in Templates
- patsnapTemplate: Full Patsnap field mappings (100+ shortcuts)
- nuvoaiTemplate: NuvoAI Patent Search fields
API Reference
createRegistry(templates: Template[])
Creates a registry with one or more templates.
Example:
const reg = createRegistry([patsnapTemplate, nuvoaiTemplate]);registry.getCompletions(text, cursor, options)
One-call UI helper. Returns { token, items }:
token:{ start, end, prefix }- the token being completeditems: Array ofCompletionItemwithinsertText,newCursor, etc.
Options:
templateId(required): Which template to uselimit?: Max number of suggestions (default: 10)trailingSpace?: Add space after completion (default: false)allowEmptyPrefix?: Show all shortcuts when prefix is empty (default: false)
registry.complete(text, cursor, options)
Get completion items for autocomplete UI. Returns CompletionItem[].
registry.suggest(prefix, templateId, limit?)
Get simple suggestions (just keys and labels). Returns Suggestion[].
registry.expand(query, options)
Expand all shortcuts in a query string.
Options:
templateId(required): Which template to usewrapBareValues?: Wrap bare values in parentheses (default: true)
registry.recordUsage(templateId, key, delta?)
Record that a shortcut was used (for ranking). delta defaults to 1.
registry.applyCompletion(text, item)
Apply a completion item to text. Returns { text, cursor }.
registry.listTemplates()
Get list of all registered templates. Returns { id, name }[].
Template Format
{
id: "patsnap",
name: "Patsnap",
normalizeKey: "upper", // or "none" - converts keys to uppercase
shortcuts: [
{
key: "TAC", // User types this
kind: "field", // "field" or "macro"
field: "TAC_ALL", // Actual field name (for kind="field")
macro: "{{value}}", // Template string (for kind="macro")
label: "Title+Abstract+Claims",
description: "Search in title, abstract, and claims",
snippet: "TAC:(|)", // | marks cursor position after insertion
aliases: ["TAC_ALL"] // Alternative keys that map to this shortcut
}
]
}Advanced Examples
Custom Template from JSON
import { loadTemplateFromJson, createRegistry } from "patent-query-shortcuts";
// Load from JSON string
const jsonString = `{
"id": "custom",
"name": "Custom Template",
"normalizeKey": "upper",
"shortcuts": [
{
"key": "TITLE",
"kind": "field",
"field": "title_field",
"snippet": "TITLE:(|)"
}
]
}`;
const template = loadTemplateFromJson(jsonString);
const reg = createRegistry([template]);Multi-Template Support
const reg = createRegistry([patsnapTemplate, nuvoaiTemplate]);
// User selects template in UI
const selectedTemplate = "patsnap";
// Get suggestions for selected template
const suggestions = reg.suggest("TA", selectedTemplate);
// Expand query using selected template
const expanded = reg.expand("TAC:(test)", { templateId: selectedTemplate });Usage Analytics
// Track which shortcuts users select
function onSuggestionSelected(templateId: string, key: string) {
registry.recordUsage(templateId, key, 1);
// Optionally save to your analytics service
analytics.track("shortcut_used", {
template: templateId,
shortcut: key
});
}
// Most-used shortcuts will now rank higher
const suggestions = registry.suggest("TA", "patsnap");
// TAC will appear before TACD if it's been used moreDate Range Handling
// Date fields automatically get range snippets
const items = registry.complete("APD", 3, { templateId: "patsnap" });
const apdItem = items.find(i => i.key === "APD");
// When applied, inserts: "APD:[ TO ]" with cursor at position 5
const result = registry.applyCompletion("APD", apdItem);
// result.text = "APD:[ TO ]"
// result.cursor = 5 (inside the bracket)License
MIT
Contributing
Contributions welcome! Please open an issue or PR.
