File Coverage

blib/lib/OTRS/SphinxSearch.pm
Criterion Covered Total %
statement 115 126 91.2
branch 34 48 70.8
condition 21 50 42.0
subroutine 11 11 100.0
pod 2 2 100.0
total 183 237 77.2


line stmt bran cond sub pod time code
1             # ABSTRACT: Implementation of the OTRS search engine by Sphinx search
2             package OTRS::SphinxSearch;
3              
4 3     3   243729 use strict;
  3         5  
  3         90  
5 3     3   10 use warnings;
  3         4  
  3         56  
6              
7 3     3   74 use 5.006001;
  3         15  
  3         81  
8              
9 3     3   12 use vars qw($VERSION);
  3         3  
  3         164  
10              
11             our $VERSION = '0.011'; # VERSION
12              
13 3     3   1912 use Sphinx::Search 0.28;
  3         197918  
  3         431  
14 3     3   1637 use Time::Piece;
  3         26201  
  3         15  
15 3     3   1843 use Readonly;
  3         7627  
  3         4605  
16              
17             Readonly my $ADDITIVE => 86_400;
18             Readonly my $SOURCE_PORT => 9312;
19             Readonly my $CONNECT_TIME_OUT => 300;
20             Readonly my $CONNECT_RETRIES => 2;
21             Readonly my $SEARCH_LIMIT => 1000;
22             Readonly my $MAX_QUERY_TIME => 0;
23             Readonly my $MATCH_MODE => SPH_MATCH_EXTENDED;
24             Readonly my $SORT_MODE => SPH_SORT_EXTENDED;
25             Readonly my $RANK_MODE => SPH_RANK_PROXIMITY_BM25;
26             Readonly my $DEFAULT_SORT_BY => 'create_time';
27             Readonly my $DEFAULT_ORDER_BY => 'DESC';
28             Readonly my $DEFAULT_RESULT_FORMAT => 'ARRAY';
29             Readonly my $SELECTED_FIELD => 'id';
30              
31              
32              
33             sub new {
34 18     18 1 44713 my ($class, %param) = @_;
35              
36 18         29 my $self = {};
37 18         36 bless $self, $class;
38              
39             # Init connection params
40 18 100       68 $self->{config} = $param{config} if defined $param{config};
41              
42 18 100       58 return unless $self->{config}->{'Index'};
43              
44 17         31 $self->{index} = $self->{config}->{'Index'};
45              
46 17         59 $self->{sphinx_object} = Sphinx::Search->new();
47              
48 17   50     947 $self->{source_host} = $self->{config}->{'SourceHost'}
49             || 'localhost';
50 17   33     97 $self->{source_port} = $self->{config}->{'SourcePort'}
51             || $SOURCE_PORT;
52 17   33     209 $self->{connect_timeout} = $self->{config}->{'ConnectTimeOut'}
53             || $CONNECT_TIME_OUT;
54 17   33     127 $self->{connect_retries} = $self->{config}->{'ConnectRetries'}
55             || $CONNECT_RETRIES;
56 17   33     123 $self->{search_limit} = $self->{config}->{'SearchLimit'}
57             || $SEARCH_LIMIT;
58 17   33     134 $self->{max_query_time} = $self->{config}->{'MaxQueryTime'}
59             || $MAX_QUERY_TIME;
60 17   33     118 $self->{match_mode} = int ($self->{config}->{'MatchMode'}
61             || $MATCH_MODE);
62 17   33     127 $self->{sort_mode} = int ($self->{config}->{'SortMode'}
63             || $SORT_MODE);
64 17   33     120 $self->{rank_mode} = int ($self->{config}->{'RankMode'}
65             || $RANK_MODE);
66 17   50     131 $self->{field_weights} = $self->{Config}->{'FieldWeights'}
67             || {};
68              
69 17   33     54 $self->{sort_by} = $self->{config}->{'SortBy'}
70             || $DEFAULT_SORT_BY;
71 17   33     127 $self->{order_by} = $self->{config}->{'OrderMode'}
72             || $DEFAULT_ORDER_BY;
73 17   33     158 $self->{selected_field} = $self->{config}->{'SelectedField'}
74             || $SELECTED_FIELD;
75 17         107 $self->{result} = $DEFAULT_RESULT_FORMAT;
76              
77              
78             # Text search params type association with a fields of index
79 17         115 $self->{text_field_map} = {
80             From => 'a_from',
81             To => 'a_to',
82             Cc => 'a_cc',
83             Subject => 'a_subject',
84             Body => 'a_body',
85             TicketNumber => 'tn',
86             Title => 'title',
87             CustomerID => 'customer_id',
88             CustomerUserLogin => 'customer_user_id',
89             };
90              
91             # Integer and MVA search params type association with a fields of index
92 17         130 $self->{uint_field_map} = {
93             TypeIDs => 'type_id',
94             StateIDs => 'ticket_state_id',
95             QueueIDs => 'queue_id',
96             CreatedQueueIDs => 'created_queue_id',
97             PriorityIDs => 'ticket_priority_id',
98             OwnerIDs => 'user_id',
99             LockIDs => 'ticket_lock_id',
100             StateTypeIDs => 'ticket_state_id',
101             WatchUserIDs => 'watch_user_id',
102             ResponsibleIDs => 'responsible_user_id',
103             ServiceIDs => 'service_id',
104             SLAIDs => 'sla_id',
105             ArchiveFlag => 'archive_flag',
106             };
107              
108             # Time associations portion
109 17         69 $self->{time_field_map} = {
110             ArticleTimeSearchType => 'ArticleCreate',
111             TimeSearchType => 'TicketCreate',
112             ChangeTimeSearchType => 'TicketChange',
113             CloseTimeSearchType => 'TicketClose',
114             EscalationTimeSearchType => 'TicketEscalation',
115             };
116 17         53 $self->{time_filter_map} = {
117             ArticleCreate => 'a_create_time',
118             TicketCreate => 'create_time',
119             TicketChange => 'change_time',
120             TicketClose => 'change_time',
121             TicketEscalation => 'escalation_time',
122             };
123 17         70 $self->{time_dimension_map} = {
124             second => 1,
125             minute => 60,
126             hour => 3600,
127             day => 86_400,
128             week => 604_800,
129             month => 2_678_400,
130             year => 31_536_000,
131             };
132              
133 17         194 $self->{sort_map} = {
134             Owner => 'user_id',
135             Responsible => 'responsible_user_id',
136             CustomerID => 'customer_id',
137             State => 'ticket_state_id',
138             Lock => 'ticket_lock_id',
139             Ticket => 'tn',
140             TicketNumber => 'tn',
141             Title => 'title',
142             Type => 'type_id',
143             Queue => 'queue_id',
144             Priority => 'ticket_priority_id',
145             Age => 'create_time',
146             Changed => 'change_time',
147             Service => 'service_id',
148             SLA => 'sla_id',
149             PendingTime => 'until_time',
150             TicketEscalation => 'escalation_time',
151             EscalationTime => 'escalation_time',
152             EscalationUpdateTime => 'escalation_update_time',
153             EscalationResponseTime => 'escalation_response_time',
154             EscalationSolutionTime => 'escalation_solution_time',
155             };
156              
157 17         37 $self->{order_map} = {
158             Up => 'ASC',
159             Down => 'DESC',
160             };
161              
162 17         43 return $self;
163             }
164              
165              
166             sub search {
167 8     8 1 4283 my ( $self, %param ) = @_;
168              
169             # Init general query settings
170 8 50       18 if ($param{'Result'}) {
171 8         13 $self->{result} = $param{'Result'};
172             }
173              
174 8 100 100     25 if ($param{'SortBy'} && $self->{sort_map}->{ $param{'SortBy'} }) {
175 1         2 $self->{sort_by} = $self->{sort_map}->{ $param{'SortBy'} };
176             }
177              
178 8 100 100     28 if ($param{'OrderBy'} && $self->{order_map}->{ $param{'OrderBy'} }) {
179 1         3 $self->{order_by} = $self->{order_map}->{ $param{'OrderBy'} };
180             }
181              
182 8 50       26 if ( $self->{sort_mode} == $SORT_MODE ) {
183 8         46 $self->{sort_expr} = "$self->{sort_by} $self->{order_by}";
184             }
185              
186             # Set query body
187 8 100       17 if ( $param{'Fulltext'} ) {
188 1         4 $self->{query} .= '@(a_from,a_to,a_cc,a_subject,a_body) ' . "$param{'Fulltext'} ";
189             }
190              
191             # Set query body additional
192 8         8 while ( my ($key, $value) = each %{ $self->{text_field_map} } ) {
  80         153  
193             # next if attribute is not used
194 72 100       103 next unless $param{$key};
195              
196 1         5 $self->{query} .= q{@} . "$value $param{$key} ";
197             }
198              
199             # Set field filters with uint values
200 8         9 while ( my ($key, $value) = each %{ $self->{uint_field_map} } ) {
  112         368  
201 104 100       178 next unless defined $param{$key};
202 3 100       10 next unless ref $param{$key} eq 'ARRAY';
203              
204             # $param{'SomeFieldIDsExclude'} = 1|0
205 2   100     11 $param{ $key . 'Exclude' } ||= 0;
206              
207             # $sph->SetFilter($attr, \@values, $exclude = 0);
208 2         14 $self->{sphinx_object}->SetFilter(
209             $value,
210 2         4 \@{ $param{$key} },
211             $param{ $key . 'Exclude' }
212             );
213             }
214              
215             # Set time filters
216 8         8 while ( my ($key, $value) = each %{ $self->{time_field_map} } ) {
  48         118  
217 40 50       60 next unless defined $param{$key};
218              
219             # If we are have more one a time filters reset it
220 0         0 ( $self->{time_start}, $self->{time_stop} ) = undef;
221              
222 0 0       0 if ( $param{$key} eq 'TimePoint' ) {
    0          
223 0   0     0 $self->_get_time_point({
224             time_point_start => $param{$value.'TimePointStart'},
225             time_point => $param{$value.'TimePoint'},
226             time_point_format => $param{$value.'TimePointFormat'},
227             time_point_base => $param{$value.'TimePointBase'} || 0,
228             });
229             }
230             elsif ( $param{$key} eq 'TimeSlot' ) {
231 0         0 $self->_get_time_slot({
232             time_start_day => $param{$value.'TimeStartDay'},
233             time_start_month => $param{$value.'TimeStartMonth'},
234             time_start_year => $param{$value.'TimeStartYear'},
235             time_stop_day => $param{$value.'TimeStopDay'},
236             time_stop_month => $param{$value.'TimeStopMonth'},
237             time_stop_year => $param{$value.'TimeStopYear'},
238             });
239             }
240              
241 0 0 0     0 if ( defined $self->{time_start} && defined $self->{time_stop} ) {
242 0         0 $self->{sphinx_object}->SetFilterRange(
243             $self->{time_filter_map}->{$value},
244             $self->{time_start},
245             $self->{time_stop},
246             );
247             }
248             }
249              
250             # Connect setup
251 8         31 $self->{sphinx_object}->SetServer( $self->{source_host}, $self->{source_port} );
252 8         160 $self->{sphinx_object}->SetConnectTimeout( $self->{connect_timeout} );
253 8         98 $self->{sphinx_object}->SetConnectRetries( $self->{connect_retries} );
254              
255             # General query settings
256 8         70 $self->{sphinx_object}->SetLimits( 0, $self->{search_limit} );
257 8         111 $self->{sphinx_object}->SetMaxQueryTime( $self->{max_query_time} );
258 8         66 $self->{sphinx_object}->SetSelect( $self->{selected_field} );
259 8         36 $self->{sphinx_object}->SetMatchMode( $self->{match_mode} );
260 8         65 $self->{sphinx_object}->SetRankingMode( $self->{rank_mode} );
261 8         59 $self->{sphinx_object}->SetFieldWeights( $self->{field_weights} );
262 8         77 $self->{sphinx_object}->SetSortMode( $self->{sort_mode}, $self->{sort_expr} );
263              
264             # Query
265 8         103 my $tickets = $self->{sphinx_object}->Query(
266             $self->{query},
267             $self->{index},
268             );
269              
270 8         223 my $returns;
271             my @ticket_ids;
272              
273             # Return count of the result
274 8 50       29 if ( $self->{result} eq 'COUNT' ) {
    50          
275 0         0 $returns->{viewable_ticket_ids} = $tickets->{total_found};
276             }
277              
278             # Return array of the result
279             elsif ( $self->{result} eq 'ARRAY' ) {
280 0         0 @ticket_ids = map { $_->{id} } @{ $tickets->{matches} };
  0         0  
  0         0  
281              
282 0         0 $returns->{viewable_ticket_ids} = \@ticket_ids;
283             }
284              
285             # True if connection failed
286 8         6 my $error = 0;
287 8 50       19 $error = 1 if $self->{sphinx_object}->IsConnectError();
288 8         35 $returns->{error} = $error;
289              
290 8         21 return $returns;
291             }
292              
293             # Private
294              
295              
296             sub _get_time_slot {
297 3     3   1103 my ( $self, $param ) = @_;
298              
299 3         5 for (qw(time_start_day time_start_month time_start_year
300             time_stop_day time_stop_month time_stop_year
301             )) {
302 13 100       30 return unless defined $param->{$_};
303             }
304              
305 2         3 my ($time_start, $time_stop);
306 2         2 eval {
307 2         18 $time_start = Time::Piece->strptime(
308             $param->{time_start_day}.'.'
309             .$param->{time_start_month}.'.'
310             .$param->{time_start_year},
311             '%d.%m.%Y',
312             );
313              
314 1         36 $time_stop = Time::Piece->strptime(
315             $param->{time_stop_day}.'.'
316             .$param->{time_stop_month}.'.'
317             .$param->{time_stop_year},
318             '%d.%m.%Y',
319             );
320             };
321 2 100       47 return if ($@);
322              
323 1         5 $self->{time_start} = $time_start->epoch;
324 1         125 $self->{time_stop} = $time_stop->epoch + $ADDITIVE;
325              
326 1         35 return $self;
327             }
328              
329              
330             sub _get_time_point {
331 5     5   1744 my ( $self, $param ) = @_;
332              
333 5         8 for ( qw(time_point_start time_point time_point_format) ) {
334 15 100       41 return unless defined $param->{$_};
335             }
336 4 100       28 return unless $param->{time_point_start} =~ m/Last|Before/;
337 2 50       7 return unless defined $self->{time_dimension_map}->{ $param->{time_point_format} };
338              
339 2         5 my $time_point =
340             $param->{time_point} * $self->{time_dimension_map}->{ $param->{time_point_format} };
341              
342 2         2 $self->{time_start} = 0;
343 2         8 $self->{time_stop} = time;
344              
345 2 100       14 if ( $param->{time_point_start} eq 'Last' ) {
    50          
346 1         3 $self->{time_start} = $self->{time_stop} - $time_point;
347             }
348             elsif ( $param->{time_point_start} eq 'Before' ) {
349 1         2 $self->{time_stop} -= $time_point;
350             }
351              
352 2         3 return $self;
353             }
354              
355             1;
356              
357             __END__