birds 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,41 @@
1
+ = Birds - Bibliographic information retrieval & document search
2
+
3
+ == VERSION
4
+
5
+ This documentation refers to Birds version 0.0.0
6
+
7
+
8
+ == DESCRIPTION
9
+
10
+ Experimental information retrieval system for bibliographic data.
11
+
12
+
13
+ == LINKS
14
+
15
+ Demo:: http://ixtrieve.fh-koeln.de/birds/litie
16
+ Documentation:: https://blackwinter.github.com/birds
17
+ Source code:: https://github.com/blackwinter/birds
18
+ RubyGem:: https://rubygems.org/gems/birds
19
+
20
+
21
+ == AUTHORS
22
+
23
+ * Jens Wille <mailto:jens.wille@gmail.com>
24
+
25
+
26
+ == LICENSE AND COPYRIGHT
27
+
28
+ Copyright (C) 2014-2015 Jens Wille
29
+
30
+ Birds is free software: you can redistribute it and/or modify it under the
31
+ terms of the GNU Affero General Public License as published by the Free
32
+ Software Foundation, either version 3 of the License, or (at your option)
33
+ any later version.
34
+
35
+ Birds is distributed in the hope that it will be useful, but WITHOUT ANY
36
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
37
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
38
+ more details.
39
+
40
+ You should have received a copy of the GNU Affero General Public License
41
+ along with Birds. If not, see <http://www.gnu.org/licenses/>.
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require File.expand_path(%q{../lib/birds/version}, __FILE__)
2
+
3
+ begin
4
+ require 'hen'
5
+
6
+ Hen.lay! {{
7
+ gem: {
8
+ name: %q{birds},
9
+ version: Birds::VERSION,
10
+ summary: %q{Bibliographic information retrieval & document search.},
11
+ description: %q{Experimental information retrieval system for bibliographic data.},
12
+ author: %q{Jens Wille},
13
+ email: %q{jens.wille@gmail.com},
14
+ license: %q{AGPL-3.0},
15
+ homepage: :blackwinter,
16
+ extra_files: FileList['*.sample', 'lib/**/{public,views}/*'].to_a,
17
+ dependencies: {
18
+ 'sinatra-bells' => ['~> 0.0', '>= 0.0.2'],
19
+ 'solr4r' => ['~> 0.0', '>= 0.0.4'],
20
+ 'unicode' => '~> 0.4'
21
+ },
22
+
23
+ required_ruby_version: '>= 1.9.3'
24
+ }
25
+ }}
26
+ rescue LoadError => err
27
+ warn "Please install the `hen' gem. (#{err})"
28
+ end
29
+
30
+ task c: :_ do
31
+ require 'irb'; IRB.start
32
+ end
33
+
34
+ task r: :_ do
35
+ load Gem.bin_path('rack', 'rackup')
36
+ end
37
+
38
+ task :_ do
39
+ ARGV.clear
40
+
41
+ b = File.expand_path('~/devel') # XXX
42
+ $:.unshift(*%w[birds solr4r sinatra-bells].map { |i| File.join(b, i, 'lib') })
43
+
44
+ require 'birds'
45
+ require 'birds/app'
46
+ end
data/config.ru.sample ADDED
@@ -0,0 +1,4 @@
1
+ require 'birds'
2
+ require 'birds/app'
3
+
4
+ run Birds::App.set(solr_core: 'birds')
@@ -0,0 +1,115 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ ###############################################################################
5
+ # #
6
+ # Birds -- Bibliographic information retrieval & document search #
7
+ # #
8
+ # Copyright (C) 2014-2015 Jens Wille #
9
+ # #
10
+ # Birds is free software: you can redistribute it and/or modify it under the #
11
+ # terms of the GNU Affero General Public License as published by the Free #
12
+ # Software Foundation, either version 3 of the License, or (at your option) #
13
+ # any later version. #
14
+ # #
15
+ # Birds is distributed in the hope that it will be useful, but WITHOUT ANY #
16
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
17
+ # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for #
18
+ # more details. #
19
+ # #
20
+ # You should have received a copy of the GNU Affero General Public License #
21
+ # along with Birds. If not, see <http://www.gnu.org/licenses/>. #
22
+ # #
23
+ ###############################################################################
24
+ #++
25
+
26
+ require 'unicode'
27
+
28
+ class Birds::App
29
+
30
+ get '/', render: :index do
31
+ result = settings.solr.count
32
+ @page_title, @page_title_extra = 'Home', "#{result.to_i} documents"
33
+ end
34
+
35
+ get '/search', render: :index do
36
+ @page_title = 'Search'
37
+
38
+ @query, @filter = params[:q] || params[:qq], Array(params[:fq])
39
+
40
+ paginate_query(@query, :results,
41
+ facet_params(debug: 'results', fq: @filter))
42
+
43
+ @explain, @facets = explain_result(@result), facet_counts(@result)
44
+ end
45
+
46
+ get '/browse', render: :browse do
47
+ @page_title, @fields = 'Browse', settings.browse_fields
48
+ end
49
+
50
+ get '/browse/:field', render: :browse do
51
+ @page_title, @field = 'Browse', params[:field]
52
+ bad_request unless labels = settings.browse_fields[@field]
53
+
54
+ @hierarchy, @page_title_extra = terms, labels.last
55
+ end
56
+
57
+ get '/browse/:field/*', render: :browse do
58
+ @page_title, @field = 'Browse', params[:field]
59
+ bad_request unless labels = settings.browse_fields[@field]
60
+
61
+ category = params[:splat].join('/')
62
+ bad_request if category.empty?
63
+
64
+ @page_title_extra = "#{labels.first}: #{category}"
65
+
66
+ @result = search_query(@field => category)
67
+ not_found if @result.empty?
68
+ end
69
+
70
+ get '/scroll', render: :scroll do
71
+ @page_title, @fields = 'Scroll', settings.scroll_fields
72
+ end
73
+
74
+ get '/scroll/:field', render: :scroll do
75
+ @page_title, @field = 'Scroll', params[:field]
76
+ bad_request unless label = settings.scroll_fields[@field]
77
+
78
+ @letters = terms.group_by { |term,|
79
+ Unicode.upcase(term[0])
80
+ }.map { |letter, values|
81
+ [letter, values.map(&:last).inject(:+)] if letter =~ /\p{Letter}/
82
+ }.compact
83
+
84
+ @page_title_extra = label
85
+ end
86
+
87
+ get '/scroll/:field/:letter', render: :scroll do
88
+ @page_title, @field = 'Scroll', params[:field]
89
+ bad_request unless label = settings.scroll_fields[@field]
90
+
91
+ @letter = params[:letter]
92
+ paginate_query({ @field => "#{@letter}*" }, :documents)
93
+
94
+ @page_title_extra = "#{label}: #{@letter}"
95
+ end
96
+
97
+ get '/document/*', render: :document do
98
+ id = params[:splat].join('/')
99
+
100
+ bad_request if id.empty?
101
+ not_found unless @document = search_document(id)
102
+
103
+ @similar = @document.more_like_this(settings.mlt_fields,
104
+ debugQuery: true, mlt: { boost: true, mintf: 1, minwl: 4 })
105
+
106
+ @explain = explain_result(@similar)
107
+
108
+ @page_title, @page_title_extra = 'Document', "##{id}"
109
+ end
110
+
111
+ get 'schema.xml' do
112
+ erb :schema
113
+ end
114
+
115
+ end
@@ -0,0 +1,110 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ ###############################################################################
5
+ # #
6
+ # Birds -- Bibliographic information retrieval & document search #
7
+ # #
8
+ # Copyright (C) 2014-2015 Jens Wille #
9
+ # #
10
+ # Birds is free software: you can redistribute it and/or modify it under the #
11
+ # terms of the GNU Affero General Public License as published by the Free #
12
+ # Software Foundation, either version 3 of the License, or (at your option) #
13
+ # any later version. #
14
+ # #
15
+ # Birds is distributed in the hope that it will be useful, but WITHOUT ANY #
16
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
17
+ # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for #
18
+ # more details. #
19
+ # #
20
+ # You should have received a copy of the GNU Affero General Public License #
21
+ # along with Birds. If not, see <http://www.gnu.org/licenses/>. #
22
+ # #
23
+ ###############################################################################
24
+ #++
25
+
26
+ class Birds::App
27
+
28
+ module Helpers
29
+
30
+ module Controller
31
+
32
+ def search(params)
33
+ settings.solr.json_query(params)
34
+ end
35
+
36
+ def search_query(query, params = {})
37
+ search(params.merge(q: query, fl: '*,score', defType: 'edismax'))
38
+ end
39
+
40
+ def search_document(id)
41
+ search(q: { id: id }).first
42
+ end
43
+
44
+ def paginate_query(query, what, query_params = {}, per_page = 20)
45
+ return unless query
46
+
47
+ page = params[:page].to_i
48
+ page = 1 if page < 1
49
+
50
+ @prev_page, @next_page = page - 1, page + 1
51
+
52
+ @result = search_query(query, query_params.merge(
53
+ rows: per_page, start: @offset = @prev_page * per_page))
54
+
55
+ @page_title_extra = '%d %s, page %d of %d' % [
56
+ @result, what, page, @total_pages = (@result.to_i / per_page.to_f).ceil]
57
+
58
+ @prev_page = nil if @prev_page < 1
59
+ @next_page = nil if @next_page > @total_pages
60
+ end
61
+
62
+ def facet_params(params = {})
63
+ params.merge(f: f = params[:f] || {}, facet: {
64
+ field: fields = [], range: ranges = [], mincount: 1
65
+ }).tap { settings.facet_fields.each { |facet, (_, options)|
66
+ options.nil? ? fields << facet : begin ranges << facet
67
+ ((f[facet] ||= {})[:facet] ||= {})[:range] = options
68
+ end
69
+ } }
70
+ end
71
+
72
+ def terms(f = @field)
73
+ settings.solr.json('terms', terms: { fl: f, limit: -1 }).to_h[f].sort
74
+ end
75
+
76
+ def explain_result(result)
77
+ result % %w[debug explain] if result
78
+ end
79
+
80
+ def facet_counts(result)
81
+ return {} unless result && result.to_i > 1
82
+
83
+ exclude, prepare = [@query, *@filter], lambda { |facet_hash, &block|
84
+ facet_hash.delete_if { |key, hash|
85
+ gap = block[hash] if block
86
+
87
+ hash.delete_if { |term,|
88
+ exclude.include?(facet_query(key, term, *gap)) }.size < 2
89
+ }
90
+ }
91
+
92
+ prepare.(result.facet_fields.to_h).merge(
93
+ prepare.(result.facet_ranges.to_h) { |hash|
94
+ counts, before, start, gap = hash
95
+ .values_at(*%w[counts before start gap])
96
+
97
+ hash.clear
98
+ hash[-start] = before if before > 1
99
+ counts.each { |value, count| hash[value.to_i] = count }
100
+
101
+ hash.singleton_class.send(:define_method, :gap) { gap }
102
+ gap
103
+ })
104
+ end
105
+
106
+ end
107
+
108
+ end
109
+
110
+ end
@@ -0,0 +1,131 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ ###############################################################################
5
+ # #
6
+ # Birds -- Bibliographic information retrieval & document search #
7
+ # #
8
+ # Copyright (C) 2014-2015 Jens Wille #
9
+ # #
10
+ # Birds is free software: you can redistribute it and/or modify it under the #
11
+ # terms of the GNU Affero General Public License as published by the Free #
12
+ # Software Foundation, either version 3 of the License, or (at your option) #
13
+ # any later version. #
14
+ # #
15
+ # Birds is distributed in the hope that it will be useful, but WITHOUT ANY #
16
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
17
+ # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for #
18
+ # more details. #
19
+ # #
20
+ # You should have received a copy of the GNU Affero General Public License #
21
+ # along with Birds. If not, see <http://www.gnu.org/licenses/>. #
22
+ # #
23
+ ###############################################################################
24
+ #++
25
+
26
+ class Birds::App
27
+
28
+ module Helpers
29
+
30
+ module View
31
+
32
+ def nav_item(path, name, title = nil)
33
+ li_(link_to(name, path, title: title), class: active?(path))
34
+ end
35
+
36
+ def link_to_document(document)
37
+ label = format_label(settings.document_label, document)
38
+ link_to(h(label), :document, document['id'])
39
+ end
40
+
41
+ def link_to_search(text, query, *filters)
42
+ options = filters.last.is_a?(Hash) ? filters.pop : {}
43
+
44
+ link_to(text, :search, options.merge(params:
45
+ query ? query_params(query, filters) : { q: filters.first }))
46
+ end
47
+
48
+ def link_to_filter(text, query, filter)
49
+ link_to_search(h(text), query, filter, *@filter)
50
+ end
51
+
52
+ def link_to_field(field, value, query = nil)
53
+ link_to_filter(value, query, field_query(field, value))
54
+ end
55
+
56
+ def link_to_facet(field, value, gap = nil)
57
+ link_to_filter(
58
+ gap ? range_label(value, gap) : value,
59
+ @query, facet_query(field, value, gap))
60
+ end
61
+
62
+ def facet_query(field, value, gap = nil)
63
+ gap ? range_query(field, value, gap) : field_query(field, value)
64
+ end
65
+
66
+ def field_query(field, value)
67
+ %Q{#{field}:"#{value}"}
68
+ end
69
+
70
+ def range_query(field, value, gap)
71
+ value < 0 ?
72
+ %Q|#{field}:[* TO #{-value}}| :
73
+ %Q|#{field}:[#{value} TO #{value + gap}}|
74
+ end
75
+
76
+ def range_label(value, gap)
77
+ value < 0 ? "before #{-value}" : "#{value}–#{value + gap - 1}"
78
+ end
79
+
80
+ def query_params(q = @query, fq = @filter)
81
+ { q: q, 'fq[]' => fq }
82
+ end
83
+
84
+ def pagination_for(*args)
85
+ params = args.last.is_a?(Hash) ? args.pop.reject { |_, v| v.nil? } : {}
86
+
87
+ ul_([
88
+ [@prev_page, :first, 1],
89
+ [@prev_page, :prev, @prev_page],
90
+ [@next_page, :next, @next_page],
91
+ [@next_page, :last, @total_pages]
92
+ ], class: 'pagination') { |condition, type, page|
93
+ li_(link_to_if(condition, pagination_icon(type),
94
+ *args, params: params.merge(page: page)),
95
+ class: disabled?(condition))
96
+ }
97
+ end
98
+
99
+ def pagination_icon(type)
100
+ tag_(:span, glyphicon(*{
101
+ first: [:fast_backward, 'First page'],
102
+ prev: [:backward, 'Previous page'],
103
+ next: [:forward, 'Next page'],
104
+ last: [:fast_forward, 'Last page']
105
+ }[type]))
106
+ end
107
+
108
+ def glyphicon(name, title = nil)
109
+ name = name.to_s.tr('_', '-')
110
+ tag_(:span, class: "glyphicon glyphicon-#{name}", title: title)
111
+ end
112
+
113
+ def values_for(key, document = @document)
114
+ Array(document[key = key.to_s]).dup.tap { |values|
115
+ return if values.empty?
116
+
117
+ if settings.browse_fields.include?(key)
118
+ values.map! { |v| link_to(v = h(v), :browse, key, v) }
119
+ elsif field = settings.linkable_fields[key]
120
+ values.map! { |v| link_to_field(field, v) }
121
+ else
122
+ values.map!(&method(:h))
123
+ end
124
+ }
125
+ end
126
+
127
+ end
128
+
129
+ end
130
+
131
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ ###############################################################################
5
+ # #
6
+ # Birds -- Bibliographic information retrieval & document search #
7
+ # #
8
+ # Copyright (C) 2014-2015 Jens Wille #
9
+ # #
10
+ # Birds is free software: you can redistribute it and/or modify it under the #
11
+ # terms of the GNU Affero General Public License as published by the Free #
12
+ # Software Foundation, either version 3 of the License, or (at your option) #
13
+ # any later version. #
14
+ # #
15
+ # Birds is distributed in the hope that it will be useful, but WITHOUT ANY #
16
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
17
+ # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for #
18
+ # more details. #
19
+ # #
20
+ # You should have received a copy of the GNU Affero General Public License #
21
+ # along with Birds. If not, see <http://www.gnu.org/licenses/>. #
22
+ # #
23
+ ###############################################################################
24
+ #++
25
+
26
+ require_relative 'helpers/controller'
27
+ require_relative 'helpers/view'
28
+
29
+ class Birds::App
30
+ helpers *Helpers.constants.map { |mod| Helpers.const_get(mod) }
31
+ end
Binary file
@@ -0,0 +1,86 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ ###############################################################################
5
+ # #
6
+ # Birds -- Bibliographic information retrieval & document search #
7
+ # #
8
+ # Copyright (C) 2014-2015 Jens Wille #
9
+ # #
10
+ # Birds is free software: you can redistribute it and/or modify it under the #
11
+ # terms of the GNU Affero General Public License as published by the Free #
12
+ # Software Foundation, either version 3 of the License, or (at your option) #
13
+ # any later version. #
14
+ # #
15
+ # Birds is distributed in the hope that it will be useful, but WITHOUT ANY #
16
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
17
+ # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for #
18
+ # more details. #
19
+ # #
20
+ # You should have received a copy of the GNU Affero General Public License #
21
+ # along with Birds. If not, see <http://www.gnu.org/licenses/>. #
22
+ # #
23
+ ###############################################################################
24
+ #++
25
+
26
+ class Birds::App
27
+
28
+ set :site_title,
29
+ 'Birds – Bibliographic information retrieval & document search'
30
+
31
+ set :bootstrap_url,
32
+ '//netdna.bootstrapcdn.com/bootstrap/3.1.1'
33
+
34
+ set :solr_client do
35
+ require 'solr4r'
36
+ Solr4R::Client
37
+ end
38
+
39
+ solr_opts = Hash[%w[host port path core].map { |key| [key, "solr_#{key}"] }]
40
+
41
+ set :solr_opts do
42
+ Hash[solr_opts.map { |key, opt| [key.to_sym, settings.send(opt)] }]
43
+ end
44
+
45
+ solr_opts.each { |key, opt| set(opt) {
46
+ settings.solr_client.const_get("DEFAULT_#{key.upcase}") } }
47
+
48
+ set :solr do
49
+ settings.solr_client.new(settings.solr_opts)
50
+ end
51
+
52
+ set_hash :display_fields, %w[
53
+ title author year abstract language theme subject
54
+ ], %w[_txt] do |f, l| [f, l.capitalize] end
55
+
56
+ set_hash :linkable_fields, %w[
57
+ author theme subject
58
+ ], %w[_txt _ss]
59
+
60
+ set_hash :facet_fields, %w[
61
+ author language theme subject
62
+ ].insert(2,
63
+ [:year, start: 1900, end: 2100, gap: 10, other: 'before']
64
+ ) do |f, o| ["#{f}_#{o ? :i : :ss}", ["#{f}s".capitalize, o]] end
65
+
66
+ set :mlt_fields, %w[
67
+ author_txt title_txt abstract_txt subject_txt
68
+ ]
69
+
70
+ set :browse_fields, {
71
+ # 'cat' => %w[Category Categories],
72
+ # 'manu_exact' => %w[Manufacturer Manufacturers]
73
+ }
74
+
75
+ set :scroll_fields, {
76
+ # 'author_s' => 'Author'
77
+ }
78
+
79
+ set :sample_queries, {
80
+ 'Sample query 1' => 'author_txt:fugmann',
81
+ 'Sample query 2' => 'fugmann -author_txt:fugmann'
82
+ }
83
+
84
+ set :document_label, '{author_ss:%s: }{title_ss( : )}{year_ss: (%s)}'
85
+
86
+ end
@@ -0,0 +1,13 @@
1
+ <ol start="<%= defined?(start) && start || 1 %>" style="margin-top: 1em"><!-- XXX -->
2
+ <% for document in documents; id, score = document['id'], h('%.2f' % document['score']); next if @document && id == @document['id'] %>
3
+ <li>
4
+ <%= link_to_document(document) %>
5
+ <% unless defined?(explain) && explain %>
6
+ <span class="badge"><%= score %></span>
7
+ <% else; eid = h("explain-#{id}") %>
8
+ <span class="badge toggle-explain" data-toggle="collapse" data-target="#<%= eid %>" title="Toggle score explanation"><%= score %></span>
9
+ <pre id="<%= eid %>" class="collapse small"><%=h explain[id] %></pre>
10
+ <% end %>
11
+ </li>
12
+ <% end %>
13
+ </ol>
@@ -0,0 +1,30 @@
1
+ <% position = 0; for facet, (label, _) in settings.facet_fields; counts = @facets[facet] or next; collapse = max = nil %>
2
+ <div class="panel panel-default">
3
+ <div class="panel-heading">
4
+ <h4 class="panel-title toggle-facet" data-toggle="collapse" data-target="#<%= fid = "facet-#{h(facet)}" %>" title="Toggle facet list"><%=h label %></h4>
5
+ </div>
6
+ <ul class="list-group compact-list-group collapse<%= ' in' if (position += 1) < 4 %>" id="<%= fid %>">
7
+ <% if counts.respond_to?(:gap) %>
8
+ <% for (value, count), index in counts.reverse_each.with_index %>
9
+ <li class="list-group-item clearfix<%= ' facet-collapse collapse' if collapse ||= index > 2 %>">
10
+ <%= link_to_facet(facet, value, counts.gap) %>
11
+ <span class="badge"><%= count %></span>
12
+ </li>
13
+ <% end %>
14
+ <% else %>
15
+ <% for (value, count), index in counts.each_with_index; max ||= count %>
16
+ <li class="list-group-item clearfix<%= ' facet-collapse collapse' if collapse ||= index > 5 || count < max / 10 %>">
17
+ <%= link_to_facet(facet, value) %>
18
+ <span class="badge"><%= count %></span>
19
+ </li>
20
+ <% end %>
21
+ <% end %>
22
+ <% if collapse %>
23
+ <li class="list-group-item toggle-facet" data-toggle="collapse" data-target="#<%= fid %> .facet-collapse.collapse">
24
+ <span class="facet-collapse collapse in">More…</span>
25
+ <span class="facet-collapse collapse">Less…</span>
26
+ </li>
27
+ <% end %>
28
+ </ul>
29
+ </div>
30
+ <% end %>
@@ -0,0 +1,22 @@
1
+ <% if @result %>
2
+ <ol>
3
+ <% for document in @result %>
4
+ <li><%= link_to_document(document) %></li>
5
+ <% end %>
6
+ </ol>
7
+ <% elsif @fields %>
8
+ <ul>
9
+ <% for field, labels in @fields %>
10
+ <li><%= link_to(h(labels.last), :browse, field) %></li>
11
+ <% end %>
12
+ </ul>
13
+ <% else %>
14
+ <ul>
15
+ <% for category, count in @hierarchy %>
16
+ <li>
17
+ <%= link_to(h(category), :browse, @field, category) %>
18
+ <span class="badge"><%=h count %></span>
19
+ </li>
20
+ <% end %>
21
+ </ul>
22
+ <% end %>
@@ -0,0 +1,12 @@
1
+ <dl class="dl-horizontal">
2
+ <% for key, label in settings.display_fields; values = values_for(key) or next %>
3
+ <dt class="text-muted" title="<%=h key %>:"><%=h label || key %></dt>
4
+ <dd><%= values.join('<br />') %></dd>
5
+ <% end %>
6
+ </dl>
7
+
8
+ <% unless @similar.empty? %>
9
+ <h2>Similar documents</h2>
10
+
11
+ <%= erb :_documents, locals: { documents: @similar, explain: @explain } %>
12
+ <% end %>