search_index_backend_spec.rb 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. require 'rails_helper'
  2. RSpec.describe SearchIndexBackend, searchindex: true do
  3. before do
  4. configure_elasticsearch
  5. rebuild_searchindex
  6. end
  7. describe '.build_query' do
  8. subject(:query) { described_class.build_query('', query_extension: params) }
  9. let(:params) { { 'bool' => { 'filter' => { 'term' => { 'a' => 'b' } } } } }
  10. it 'coerces :query_extension hash keys to symbols' do
  11. expect(query.dig(:query, :bool, :filter, :term, :a)).to eq('b')
  12. end
  13. end
  14. describe '.search' do
  15. context 'query finds results' do
  16. let(:record_type) { 'Ticket'.freeze }
  17. let(:record) { create :ticket }
  18. before do
  19. described_class.add(record_type, record)
  20. described_class.refresh
  21. end
  22. it 'finds added records' do
  23. result = described_class.search(record.number, record_type, sort_by: ['updated_at'], order_by: ['desc'])
  24. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  25. end
  26. end
  27. context 'for query with no results' do
  28. subject(:search) { described_class.search(query, index, limit: 3000) }
  29. let(:query) { 'preferences.notification_sound.enabled:*' }
  30. context 'on a single index' do
  31. let(:index) { 'User' }
  32. it { is_expected.to be_an(Array).and be_empty }
  33. end
  34. context 'on multiple indices' do
  35. let(:index) { %w[User Organization] }
  36. it { is_expected.to be_an(Array).and not_include(nil).and be_empty }
  37. end
  38. end
  39. end
  40. describe '.append_wildcard_to_simple_query' do
  41. context 'with "simple" queries' do
  42. let(:queries) { <<~QUERIES.lines.map { |x| x.split('#')[0] }.map(&:strip) }
  43. M
  44. Max
  45. Max. # dot and underscore are acceptable characters in simple queries
  46. A_
  47. A_B
  48. äöü
  49. 123
  50. *ax # wildcards are allowed in simple queries
  51. Max*
  52. M*x
  53. M?x
  54. test@example.com
  55. test@example.
  56. test@example
  57. test@
  58. QUERIES
  59. it 'appends a * to the original query' do
  60. expect(queries.map(&described_class.method(:append_wildcard_to_simple_query)))
  61. .to eq(queries.map { |q| "#{q}*" })
  62. end
  63. end
  64. context 'with "complex" queries (using search operators)' do
  65. let(:queries) { <<~QUERIES.lines.map { |x| x.split('#')[0] }.map(&:strip) }
  66. title:"some words with spaces" # exact phrase / without quotation marks " an AND search for the words will be performed (in Zammad 1.5 and lower an OR search will be performed)
  67. title:"some wor*" # exact phrase beginning with "some wor*" will be searched
  68. created_at:[2017-01-01 TO 2017-12-31] # a time range
  69. created_at:>now-1h # created within last hour
  70. state:new OR state:open
  71. (state:new OR state:open) OR priority:"3 normal"
  72. (state:new OR state:open) AND customer.lastname:smith
  73. state:(new OR open) AND title:(full text search) # state: new OR open & title: full OR text OR search
  74. tags: "some tag"
  75. owner.email: "bod@example.com" AND state: (new OR open OR pending*) # show all open tickets of a certain agent
  76. state:closed AND _missing_:tag # all closed objects without tags
  77. article_count: [1 TO 5] # tickets with 1 to 5 articles
  78. article_count: [10 TO *] # tickets with 10 or more articles
  79. article.from: bob # also article.from can be used
  80. article.body: heat~ # using the fuzzy operator will also find terms that are similar, in this case also "head"
  81. article.body: /joh?n(ath[oa]n)/ # using regular expressions
  82. user:M
  83. user:Max
  84. user:Max.
  85. user:Max*
  86. organization:A_B
  87. organization:A_B*
  88. user: M
  89. user: Max
  90. user: Max.
  91. user: Max*
  92. organization: A_B
  93. organization: A_B*
  94. id:123
  95. number:123
  96. id:"123"
  97. number:"123"
  98. QUERIES
  99. it 'returns the original query verbatim' do
  100. expect(queries.map(&described_class.method(:append_wildcard_to_simple_query)))
  101. .to eq(queries)
  102. end
  103. end
  104. end
  105. describe '.remove' do
  106. context 'record gets deleted' do
  107. let(:record_type) { 'Ticket'.freeze }
  108. let(:deleted_record) { create :ticket }
  109. before do
  110. described_class.add(record_type, deleted_record)
  111. described_class.refresh
  112. end
  113. it 'removes record from search index' do
  114. described_class.remove(record_type, deleted_record.id)
  115. described_class.refresh
  116. result = described_class.search(deleted_record.number, record_type, sort_by: ['updated_at'], order_by: ['desc'])
  117. expect(result).to eq([])
  118. end
  119. context 'other records present' do
  120. let(:other_record) { create :ticket }
  121. before do
  122. described_class.add(record_type, other_record)
  123. described_class.refresh
  124. end
  125. it "doesn't remove other records" do
  126. described_class.remove(record_type, deleted_record.id)
  127. described_class.refresh
  128. result = described_class.search(other_record.number, record_type, sort_by: ['updated_at'], order_by: ['desc'])
  129. expect(result).to eq([{ id: other_record.id.to_s, type: record_type }])
  130. end
  131. end
  132. end
  133. end
  134. describe '.selectors' do
  135. let(:ticket1) { create :ticket, title: 'some-title1', state_id: 1 }
  136. let(:ticket2) { create :ticket, title: 'some_title2', state_id: 4 }
  137. let(:ticket3) { create :ticket, title: 'some::title3', state_id: 1 }
  138. let(:ticket4) { create :ticket, title: 'phrase some-title4', state_id: 1 }
  139. let(:ticket5) { create :ticket, title: 'phrase some_title5', state_id: 1 }
  140. let(:ticket6) { create :ticket, title: 'phrase some::title6', state_id: 1 }
  141. let(:ticket7) { create :ticket, title: 'some title7', state_id: 1 }
  142. let(:ticket8) { create :ticket, title: 'sometitle', state_id: 1 }
  143. before do
  144. Ticket.destroy_all # needed to remove not created tickets
  145. described_class.add('Ticket', ticket1)
  146. travel 1.second
  147. described_class.add('Ticket', ticket2)
  148. travel 1.second
  149. described_class.add('Ticket', ticket3)
  150. travel 1.second
  151. described_class.add('Ticket', ticket4)
  152. travel 1.second
  153. described_class.add('Ticket', ticket5)
  154. travel 1.second
  155. described_class.add('Ticket', ticket6)
  156. travel 1.second
  157. described_class.add('Ticket', ticket7)
  158. travel 1.second
  159. described_class.add('Ticket', ticket8)
  160. described_class.refresh
  161. end
  162. context 'query with contains' do
  163. it 'finds records with containing phrase' do
  164. result = described_class.selectors('Ticket',
  165. {
  166. 'title' => {
  167. 'operator' => 'contains',
  168. 'value' => 'phrase',
  169. },
  170. },
  171. {},
  172. {
  173. field: 'created_at', # sort to verify result
  174. })
  175. expect(result).to eq({ count: 3, ticket_ids: [ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s] })
  176. end
  177. it 'finds records with containing some title7' do
  178. result = described_class.selectors('Ticket',
  179. 'title' => {
  180. 'operator' => 'contains',
  181. 'value' => 'some title7',
  182. })
  183. expect(result).to eq({ count: 1, ticket_ids: [ticket7.id.to_s] })
  184. end
  185. it 'finds records with containing -' do
  186. result = described_class.selectors('Ticket',
  187. 'title' => {
  188. 'operator' => 'contains',
  189. 'value' => 'some-title1',
  190. })
  191. expect(result).to eq({ count: 1, ticket_ids: [ticket1.id.to_s] })
  192. end
  193. it 'finds records with containing _' do
  194. result = described_class.selectors('Ticket',
  195. 'title' => {
  196. 'operator' => 'contains',
  197. 'value' => 'some_title2',
  198. })
  199. expect(result).to eq({ count: 1, ticket_ids: [ticket2.id.to_s] })
  200. end
  201. it 'finds records with containing ::' do
  202. result = described_class.selectors('Ticket',
  203. 'title' => {
  204. 'operator' => 'contains',
  205. 'value' => 'some::title3',
  206. })
  207. expect(result).to eq({ count: 1, ticket_ids: [ticket3.id.to_s] })
  208. end
  209. it 'finds records with containing 4' do
  210. result = described_class.selectors('Ticket',
  211. 'state_id' => {
  212. 'operator' => 'contains',
  213. 'value' => 4,
  214. })
  215. expect(result).to eq({ count: 1, ticket_ids: [ticket2.id.to_s] })
  216. end
  217. it 'finds records with containing "4"' do
  218. result = described_class.selectors('Ticket',
  219. 'state_id' => {
  220. 'operator' => 'contains',
  221. 'value' => '4',
  222. })
  223. expect(result).to eq({ count: 1, ticket_ids: [ticket2.id.to_s] })
  224. end
  225. end
  226. context 'query with contains not' do
  227. it 'finds records with containing not phrase' do
  228. result = described_class.selectors('Ticket',
  229. {
  230. 'title' => {
  231. 'operator' => 'contains not',
  232. 'value' => 'phrase',
  233. },
  234. },
  235. {},
  236. {
  237. field: 'created_at', # sort to verify result
  238. })
  239. expect(result).to eq({ count: 5, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  240. end
  241. it 'finds records with containing not some title7' do
  242. result = described_class.selectors('Ticket',
  243. 'title' => {
  244. 'operator' => 'contains not',
  245. 'value' => 'some title7',
  246. })
  247. expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  248. end
  249. it 'finds records with containing not -' do
  250. result = described_class.selectors('Ticket',
  251. 'title' => {
  252. 'operator' => 'contains not',
  253. 'value' => 'some-title1',
  254. })
  255. expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s] })
  256. end
  257. it 'finds records with containing not _' do
  258. result = described_class.selectors('Ticket',
  259. 'title' => {
  260. 'operator' => 'contains not',
  261. 'value' => 'some_title2',
  262. })
  263. expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket1.id.to_s] })
  264. end
  265. it 'finds records with containing not ::' do
  266. result = described_class.selectors('Ticket',
  267. 'title' => {
  268. 'operator' => 'contains not',
  269. 'value' => 'some::title3',
  270. })
  271. expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  272. end
  273. it 'finds records with containing not 4' do
  274. result = described_class.selectors('Ticket',
  275. 'state_id' => {
  276. 'operator' => 'contains not',
  277. 'value' => 4,
  278. })
  279. expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket1.id.to_s] })
  280. end
  281. it 'finds records with containing not "4"' do
  282. result = described_class.selectors('Ticket',
  283. 'state_id' => {
  284. 'operator' => 'contains not',
  285. 'value' => '4',
  286. })
  287. expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket1.id.to_s] })
  288. end
  289. end
  290. context 'query with is' do
  291. it 'finds records with is phrase' do
  292. result = described_class.selectors('Ticket',
  293. 'title' => {
  294. 'operator' => 'is',
  295. 'value' => 'phrase',
  296. })
  297. expect(result).to eq({ count: 0, ticket_ids: [] })
  298. end
  299. it 'finds records with is some title7' do
  300. result = described_class.selectors('Ticket',
  301. 'title' => {
  302. 'operator' => 'is',
  303. 'value' => 'some title7',
  304. })
  305. expect(result).to eq({ count: 1, ticket_ids: [ticket7.id.to_s] })
  306. end
  307. it 'finds records with is -' do
  308. result = described_class.selectors('Ticket',
  309. 'title' => {
  310. 'operator' => 'is',
  311. 'value' => 'some-title1',
  312. })
  313. expect(result).to eq({ count: 1, ticket_ids: [ticket1.id.to_s] })
  314. end
  315. it 'finds records with is _' do
  316. result = described_class.selectors('Ticket',
  317. 'title' => {
  318. 'operator' => 'is',
  319. 'value' => 'some_title2',
  320. })
  321. expect(result).to eq({ count: 1, ticket_ids: [ticket2.id.to_s] })
  322. end
  323. it 'finds records with is ::' do
  324. result = described_class.selectors('Ticket',
  325. 'title' => {
  326. 'operator' => 'is',
  327. 'value' => 'some::title3',
  328. })
  329. expect(result).to eq({ count: 1, ticket_ids: [ticket3.id.to_s] })
  330. end
  331. it 'finds records with is 4' do
  332. result = described_class.selectors('Ticket',
  333. 'state_id' => {
  334. 'operator' => 'is',
  335. 'value' => 4,
  336. })
  337. expect(result).to eq({ count: 1, ticket_ids: [ticket2.id.to_s] })
  338. end
  339. it 'finds records with is "4"' do
  340. result = described_class.selectors('Ticket',
  341. 'state_id' => {
  342. 'operator' => 'is',
  343. 'value' => '4',
  344. })
  345. expect(result).to eq({ count: 1, ticket_ids: [ticket2.id.to_s] })
  346. end
  347. end
  348. context 'query with is not' do
  349. it 'finds records with is not phrase' do
  350. result = described_class.selectors('Ticket',
  351. 'title' => {
  352. 'operator' => 'is not',
  353. 'value' => 'phrase',
  354. })
  355. expect(result).to eq({ count: 8, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  356. end
  357. it 'finds records with is not some title7' do
  358. result = described_class.selectors('Ticket',
  359. 'title' => {
  360. 'operator' => 'is not',
  361. 'value' => 'some title7',
  362. })
  363. expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  364. end
  365. it 'finds records with is not -' do
  366. result = described_class.selectors('Ticket',
  367. 'title' => {
  368. 'operator' => 'is not',
  369. 'value' => 'some-title1',
  370. })
  371. expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s] })
  372. end
  373. it 'finds records with is not _' do
  374. result = described_class.selectors('Ticket',
  375. 'title' => {
  376. 'operator' => 'is not',
  377. 'value' => 'some_title2',
  378. })
  379. expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket1.id.to_s] })
  380. end
  381. it 'finds records with is not ::' do
  382. result = described_class.selectors('Ticket',
  383. 'title' => {
  384. 'operator' => 'is not',
  385. 'value' => 'some::title3',
  386. })
  387. expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  388. end
  389. it 'finds records with is not 4' do
  390. result = described_class.selectors('Ticket',
  391. 'state_id' => {
  392. 'operator' => 'is not',
  393. 'value' => 4,
  394. })
  395. expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket1.id.to_s] })
  396. end
  397. it 'finds records with is not "4"' do
  398. result = described_class.selectors('Ticket',
  399. 'state_id' => {
  400. 'operator' => 'is not',
  401. 'value' => '4',
  402. })
  403. expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket1.id.to_s] })
  404. end
  405. it 'finds records with is not state_id ["4"] and title ["sometitle"]' do
  406. result = described_class.selectors('Ticket',
  407. 'state_id' => {
  408. 'operator' => 'is not',
  409. 'value' => ['4'],
  410. },
  411. 'title' => {
  412. 'operator' => 'is',
  413. 'value' => ['sometitle'],
  414. })
  415. expect(result).to eq({ count: 1, ticket_ids: [ticket8.id.to_s] })
  416. end
  417. end
  418. end
  419. end