birds 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 %>