Skip to content

Commit f670d24

Browse files
authored
Find Similar Tickets without using NLP or AI (#2356)
* Create findSimilarTickets.js This script helps the users to find tickets that have similar ticket descriptions and short descriptions and score as per their matched content which can be utilised in reports to see how many tickets are having similarities and further take necessary actions like creating a problem ticket with these details. * Create README.md
1 parent 9fb5b58 commit f670d24

File tree

2 files changed

+114
-0
lines changed

2 files changed

+114
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
This script identifies tickets similar to a given ticket in ServiceNow based on text similarity of short_description and description fields, optionally boosting the score for matching categories or assignment groups. It is intended for background script execution for testing, analysis, or automation purposes.
2+
3+
**Features:**
4+
1) Compares a source ticket against other tickets in the same table (incident by default).
5+
2) Computes Jaccard similarity between tokenized text fields.
6+
3) Applies bonus points for matching category and assignment_group.
7+
4) Returns a sorted list of similar tickets with score, number, caller_id and short_description.
8+
5) Supports top N results and a minimum score filter.
9+
10+
11+
**Usage:**
12+
1) Paste the script in scripts-background.
13+
2) Make changes to the sys_id of your ticket in line no. 3
14+
3) click run to get an output as below
15+
16+
**Output:**
17+
* *** Script: === Similar Tickets to: INC0010005 ===
18+
* *** Script: INC0010006 | Score: 1.08 | undefined | Sai Test INC0008112
19+
* *** Script: INC0010004 | Score: 0.58 | undefined | Sai Test INC0009005
20+
* *** Script: INC0009009 | Score: 0.161 | undefined | Unable to access the shared folder.test
21+
* *** Script: INC0008001 | Score: 0.08 | undefined | ATF:TEST2
22+
* *** Script: INC0000020 | Score: 0.08 | undefined | I need a replacement iPhone, please
23+
* *** Script: INC0000031 | Score: 0.08 | undefined | Need help with Remedy. Can we configure UI?
24+
* *** Script: INC0000040 | Score: 0.08 | undefined | JavaScript error on hiring page of corporate website
25+
* *** Script: INC0010002 | Score: 0.08 | undefined |
26+
* *** Script: INC0000057 | Score: 0.08 | undefined | Performance problems with wifi
27+
* *** Script: INC0010003 | Score: 0.08 | undefined |
28+
29+
**Explanation of the output:**
30+
1) First Line contains the ticket which you have provided as a sys_id.
31+
2) Next lines contains the ticket which contain ticket no. | score | caller_id | short_description.
32+
3) If you keenly observe there are few tickets that do not have similar short description / description with scores as 0.08 but still in output the reason for this is their category and assignment group still matches with the compared ticket.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
(function() {
2+
var table = 'incident'; //can be used for other tickets as well
3+
var sourceSysId = 'f4755b82c3203210348bbd33e40131cb'; // sys_id of the ticket which is used to find similar tickets
4+
var limit = 10; // top N results
5+
var minScore = 0.05;
6+
7+
function tokensFromText(text) {
8+
if (!text) return [];
9+
text = text.toLowerCase().replace(/[^a-z0-9\s]/g, ' ');
10+
var raw = text.split(/\s+/);
11+
var stop = {
12+
'the':1,'and':1,'for':1,'with':1,'that':1,'this':1,'from':1,'have':1,'has':1,'was':1,'were':1,
13+
'a':1,'an':1,'is':1,'in':1,'on':1,'of':1,'to':1,'it':1,'as':1,'by':1,'be':1,'are':1
14+
};
15+
var map = {};
16+
for (var i=0;i<raw.length;i++) {
17+
var t = raw[i].trim();
18+
if (!t || t.length<3 || stop[t]) continue; // skip very short tokens
19+
t = t.replace(/(ing|ed|s)$/,'');
20+
map[t] = (map[t]||0)+1;
21+
}
22+
return Object.keys(map);
23+
}
24+
25+
function jaccardScore(tokensA, tokensB) {
26+
var setA={}, setB={};
27+
tokensA.forEach(function(t){setA[t]=1;});
28+
tokensB.forEach(function(t){setB[t]=1;});
29+
var inter=0, uni=0;
30+
for (var t in setA){ if(setB[t]) inter++; uni++; }
31+
for (var t2 in setB){ if(!setA[t2]) uni++; }
32+
return uni===0 ? 0 : inter/uni;
33+
}
34+
35+
// Get source record
36+
var src = new GlideRecord(table);
37+
if (!src.get(sourceSysId)) {
38+
gs.error("Source record not found");
39+
return;
40+
}
41+
42+
var sourceText = [src.short_description, src.description].join(" ");
43+
var sourceTokens = tokensFromText(sourceText);
44+
if (sourceTokens.length === 0) {
45+
gs.print("No meaningful text to compare");
46+
return;
47+
}
48+
49+
// Find candidate incidents
50+
var gr = new GlideRecord(table);
51+
gr.addActiveQuery();
52+
gr.addQuery('sys_id','!=',sourceSysId);
53+
gr.setLimit(300);
54+
gr.query();
55+
56+
var results = [];
57+
while (gr.next()) {
58+
var candidateText = [gr.short_description, gr.description].join(" ");
59+
var candidateTokens = tokensFromText(candidateText);
60+
var score = jaccardScore(sourceTokens, candidateTokens);
61+
62+
if (src.category == gr.category) score += 0.05;
63+
if (src.assignment_group == gr.assignment_group) score += 0.03;
64+
65+
if (score >= minScore) {
66+
results.push({
67+
number: gr.number.toString(),
68+
short_description: gr.short_description.toString(),
69+
score: parseFloat(score.toFixed(3))
70+
});
71+
}
72+
}
73+
74+
results.sort(function(a,b){return b.score - a.score;});
75+
76+
// Print top results
77+
gs.print("=== Similar Tickets to: " + src.number + " ===");
78+
results.slice(0, limit).forEach(function(r) {
79+
gs.print(r.number + " | Score: " + r.score + " | " + r.caller_id + " | " + r.short_description);
80+
});
81+
82+
})();

0 commit comments

Comments
 (0)