jekyll-related-posts 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d2802d28e9109784b9d9aed5987459d7c085b29e
4
+ data.tar.gz: b4ec98641cbe32e590b2a6aa428013c5aa50b266
5
+ SHA512:
6
+ metadata.gz: 70ad01243ce8f17f133d3c56cf2d55bb5316881b2fb50d13c442fb0cb9953528a07a2dde7c304ce4b9ecda543c1bb83257923b96f628697b93804cd3af5320f1
7
+ data.tar.gz: d0c17c9d025e12c51df2ce7d4ddfe0a6d15baf63454a6e09052906342d4acdf8d5e1d4c2bfb1adf970eedf6eb0621a7f60cfcfb7c3a107da67c0ec0a380016b7
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jekyll-related-posts.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Amadeusz Juskowiak
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,95 @@
1
+ # jekyll-related-posts
2
+
3
+ Proper related posts plugin for [Jekyll](http://jekyllrb.com) - uses document correlation matrix on TF-IDF (optionally with Latent Semantic Indexing).
4
+
5
+ ## Example
6
+
7
+ Example is provided at http://jekyll-related-posts.dev.amadeusz.me - posts are
8
+ based on [Reuters-21578](https://archive.ics.uci.edu/ml/datasets/Reuters-21578+Text+Categorization+Collection) data set.
9
+
10
+ ## Introduction
11
+
12
+ I am going to try to start blogging, again. Anyway I am studying at
13
+ Decision Support Systems Group and I have found document correlation
14
+ problem somehow interesting.
15
+
16
+ For my own purposes I have created related posts Jekyll plugin based on well
17
+ known algorithms such as [TFIDF](https://en.wikipedia.org/wiki/Tf–idf)
18
+ and [LSI](https://en.wikipedia.org/wiki/Latent_semantic_indexing).
19
+
20
+ ## How to install
21
+
22
+ Initialy you had to install the plugin manually, however the plugin is a
23
+ gem now - follow instructions to install the plugin:
24
+
25
+ 1. Install the gem `jekyll-related-posts`:
26
+ - if you are using bundler add `gem 'jekyll-related-posts'` to your
27
+ `Gemfile` and run `bundle install`,
28
+ - or install gem via `gem install jekyll-related-posts`.
29
+ 2. Insert `gems: ['jekyll-related-posts']` to your `_config.yml`.
30
+ 3. Insert `<related-posts />` somewhere in your `_layouts/post.html`
31
+ file.
32
+ 4. Run `jekyll build`, don't forget to blog about the plugin!
33
+
34
+ ### Customization
35
+
36
+ You can customize default related posts template by creating
37
+ `related.html` in your layouts directory. Plugin behaviour can be
38
+ altered by options in `_config.yml`, under `related:` section.
39
+
40
+ ## Basis of operation
41
+
42
+ Each document is
43
+ [tokenized](https://en.wikipedia.org/wiki/Tokenization_(lexical_analysis))
44
+ and [stemmed](https://en.wikipedia.org/wiki/Stemming), every word found
45
+ is treated as keyword for analysis (except for some [stop
46
+ words](https://en.wikipedia.org/wiki/Stop_words)).
47
+
48
+ TF-IDF matrix for the whole site is calculated (including extra provided
49
+ weights), then if given accuraccy is lower than 1.0, LSI algorithm
50
+ is used to compute new simplified vector space. Document correlation
51
+ matrix is created using dot product of the matrix and its transpose.
52
+
53
+ For each of the post' related documents are inserted into priority queue
54
+ (sorted by score from document correlation matrix), assuming the score
55
+ is greater than minimal required score. Selected few bests related posts
56
+ are retrieven from the queue.
57
+
58
+ Liquid template for each post is rendered and `<related-posts />` is
59
+ replaced with the outcomes of algorithm.
60
+
61
+ ## Configuration
62
+
63
+ In your `_config.yml` file (under `related:`) you can configure:
64
+
65
+ - `max_count: 5` - maximum number of related posts,
66
+ - `min_score: 0.1` - minimal required score to treat post as related,
67
+ - `accuracy: 0.75` - percentage of keywords used as basis for document
68
+ correlation matrix (if 1.0 then no LSI is computed, otherwise LSI is
69
+ computed and dimensions are reduced to `accuracy * |keywords|`)
70
+
71
+ ### Weights
72
+
73
+ You can configure weights of words providing dictionary with them to
74
+ `weights`. In example weight of `2` means for term frequency algorithm
75
+ that the word occured twice as much in the document as in reality.
76
+
77
+ ## Benchmark
78
+
79
+ For casual blogs, performance should not be an issue.
80
+
81
+ I did not benchmark the plugin, however for the dataset given in the
82
+ example (containing ~900 documents, ~7000 keywords) rendering time
83
+ (including Jekyll hoodoo stuff) is more less 70 seconds (on Xeon, using
84
+ 750MB RAM). Computation related to this plugin is about 20 seconds
85
+ long. It should be noticed that I'm using OpenBLAS and standard LAPACK
86
+ distributed with Ubuntu (performance is similar on OS X using builtin
87
+ Acccelerate framework).
88
+
89
+ Unfortunately the plugin is not compatible with Jekyll 3.0 new
90
+ incremental builds, even though it requires at least Jekyll 3.0 (for the
91
+ plugin hooks).
92
+
93
+ ## Authors
94
+
95
+ - Amadeusz Juskowiak - juskowiak[at]amadeusz.me
@@ -0,0 +1,38 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "jekyll-related-posts"
7
+ spec.version = "0.1.1"
8
+ spec.authors = ["Amadeusz Juskowiak"]
9
+ spec.email = ["juskowiak@amadeusz.me"]
10
+ spec.summary = %q{Proper related posts plugin for Jekyll - uses document correlation matrix on TF-IDF (optionally with Latent Semantic Indexing).}
11
+ spec.description = %q{Proper related posts plugin for Jekyll - uses document correlation matrix on TF-IDF (optionally with Latent Semantic Indexing).
12
+
13
+ Each document is tokenized and stemmed, every word found is treated as keyword for analysis (except for some stop words).
14
+
15
+ TF-IDF matrix for the whole site is calculated (including extra provided weights), then if given accuraccy is lower than 1.0, LSI algorithm is used to compute new simplified vector space. Document correlation matrix is created using dot product of the matrix and its transpose.
16
+
17
+ For each of the post' related documents are inserted into priority queue (sorted by score from document correlation matrix), assuming the score is greater than minimal required score. Selected few bests related posts are retrieven from the queue.
18
+
19
+ Liquid template for each post is rendered and <related-posts /> is replaced with the outcomes of algorithm.}
20
+ spec.homepage = "https://github.com/alfanick/jekyll-related-posts"
21
+ spec.license = "MIT"
22
+
23
+ spec.files = `git ls-files -z`.split("\x0")
24
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
25
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.6"
29
+
30
+ spec.add_runtime_dependency "jekyll", "~> 3.0"
31
+ spec.add_runtime_dependency "liquid", "~> 3.0"
32
+ spec.add_runtime_dependency "tokenizer", "~> 0.1"
33
+ spec.add_runtime_dependency "stopwords-filter", "~> 0.3"
34
+ spec.add_runtime_dependency "fast-stemmer", "~> 1.0"
35
+ spec.add_runtime_dependency "pqueue", "~> 2.1"
36
+ spec.add_runtime_dependency "nmatrix", "~> 0.2"
37
+ spec.add_runtime_dependency "nmatrix-lapacke", "~> 0.2"
38
+ end
@@ -0,0 +1,5 @@
1
+ related:
2
+ max_count: 5
3
+ min_score: 0.1
4
+ accuracy: 0.75
5
+ weights: {}
@@ -0,0 +1,220 @@
1
+ require 'rubygems'
2
+ require 'jekyll'
3
+ require 'singleton'
4
+ require 'tokenizer'
5
+ require 'yaml'
6
+ require 'liquid'
7
+ require 'fast_stemmer'
8
+ require 'stopwords'
9
+ require 'pqueue'
10
+ require 'nmatrix'
11
+ require 'nmatrix/lapacke'
12
+
13
+ module Amadeusz
14
+ module Jekyll
15
+ class RelatedPosts
16
+ include Singleton
17
+
18
+ def initialize
19
+ @posts = Array.new
20
+ @keywords = Array.new
21
+ @tokenizer = Tokenizer::Tokenizer.new(:en)
22
+ @stopwords_filter = Stopwords::Snowball::Filter.new('en')
23
+ end
24
+
25
+ def add_post(post)
26
+ post = {
27
+ url: post.url,
28
+ title: post.data['title'].dup,
29
+ content: (stem(post.content) + stem(post.data['title']))
30
+ }
31
+
32
+ @posts << post
33
+ @keywords += post[:content]
34
+ @keywords.uniq!
35
+ end
36
+
37
+ def build!(site)
38
+ conf = config(site)
39
+ @weights = keywords_weights(conf['weights'])
40
+ related = find_releated(conf['max_count'], conf['min_score'], conf['accuracy'])
41
+ template = Liquid::Template.parse(File.read(template_path(site)))
42
+
43
+ @posts.each do |post|
44
+ filename = File.join(site.config['destination'], post[:url])
45
+ rendered = File.read(filename)
46
+
47
+ output = template.render('related_posts' => related[post])
48
+
49
+ rendered.gsub! '<related-posts />', output
50
+ File.write(filename, rendered)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def config(site)
57
+ builtin_file = File.join(File.absolute_path(File.dirname(__FILE__)), '_config.yml')
58
+ defaults = YAML.load_file(builtin_file)
59
+
60
+ defaults['related'].merge(site.config['related'] || {})
61
+ end
62
+
63
+ def template_path(site)
64
+ site_file = File.join(site.config['source'], site.config['layouts_dir'], 'related.html')
65
+ builtin_file = File.join(File.absolute_path(File.dirname(__FILE__)), 'related.html')
66
+
67
+ if File.exist? site_file
68
+ site_file
69
+ else
70
+ builtin_file
71
+ end
72
+ end
73
+
74
+ def find_releated(count = 5, min_score = -10.0, accuracy = 1.0)
75
+ dc = document_correleation(accuracy)
76
+ result = Hash.new
77
+ count = [count, @posts.size].min
78
+
79
+ @posts.each_with_index do |post, index|
80
+ queue = PQueue.new(dc.row(index).each_with_index.select{|s,_| s>=min_score}) do |a, b|
81
+ a[0] > b[0]
82
+ end
83
+
84
+ result[post] = []
85
+ count.times do
86
+ score, id = queue.pop
87
+ break unless score
88
+ begin
89
+ result[post] << {
90
+ 'score' => score,
91
+ 'url' => @posts[id][:url],
92
+ 'title' => @posts[id][:title]
93
+ }
94
+ rescue
95
+ break
96
+ end
97
+ end
98
+ end
99
+
100
+ return result
101
+ end
102
+
103
+ def lsi(matrix, accuracy)
104
+ degree = (@keywords.size * accuracy - 1).floor
105
+ u, sigma, vt = matrix.transpose.gesdd
106
+
107
+ u2 = u.slice(0..degree, 0..degree)
108
+ sigma_d = NMatrix.zeros([degree+1, @posts.size])
109
+ sigma.each_with_indices do |v, i, j|
110
+ break if i > degree
111
+ sigma_d[i, i] = v
112
+ end
113
+
114
+ return u2.dot(sigma_d).dot(vt).transpose
115
+ end
116
+
117
+ def document_correleation(accuracy = 1.0)
118
+ if accuracy == 1.0
119
+ scores = tfidf
120
+ else
121
+ scores = lsi(tfidf, accuracy)
122
+ end
123
+
124
+ result = scores.dot(scores.transpose)
125
+
126
+ result.each_with_indices do |_, u, v|
127
+ if u != v
128
+ result[u, v] /= (result[u, u] + result[v, v] - result[u, v])
129
+ else
130
+ result[u, v] = 0.0
131
+ end
132
+ end
133
+
134
+ return result
135
+ end
136
+
137
+ def bag_of_words
138
+ result = NMatrix.new([@posts.size, @keywords.size], 0.0)
139
+ @max = NMatrix.new([@posts.size], 0.0)
140
+
141
+ result.each_with_indices do |_, pi, ki|
142
+ result[pi, ki] = @posts[pi][:content].count(@keywords[ki])
143
+
144
+ if result[pi, ki] > @max[pi]
145
+ @max[pi] = result[pi, ki]
146
+ end
147
+ end
148
+
149
+ @bag_of_words = result.dup
150
+ return result
151
+ end
152
+
153
+ def term_frequency
154
+ result = bag_of_words
155
+
156
+ result.rows.times do |r|
157
+ result[r, 0..-1] *= @weights
158
+ result[r, 0..-1] /= @max[r]
159
+ end
160
+
161
+ return result
162
+ end
163
+
164
+ def keywords_weights(weights)
165
+ result = NMatrix.new([1, @keywords.size], 1.0)
166
+
167
+ weights.each do |word, weight|
168
+ keyword = word.to_s.stem.to_sym
169
+
170
+ next unless @keywords.include? keyword
171
+
172
+ result[0, @keywords.index(keyword)] = weight
173
+ end
174
+
175
+ return result
176
+ end
177
+
178
+ def inverse_document_frequency
179
+ result = NMatrix.new([1, @keywords.size], 0.0)
180
+
181
+ @bag_of_words.each_column do |column|
182
+ occurences = column.reduce do |m, c|
183
+ m + (c > 0 ? 1.0 : 0.0)
184
+ end
185
+
186
+ result[0, column.offset[1]] = Math.log(column.size / occurences) if occurences > 0
187
+ end
188
+
189
+ return result
190
+ end
191
+
192
+ def tfidf
193
+ result = term_frequency
194
+ idf = inverse_document_frequency
195
+
196
+ result.rows.times do |r|
197
+ result[r, 0..-1] *= idf
198
+ end
199
+
200
+ return result
201
+ end
202
+
203
+ def stem(data)
204
+ tokenized = @tokenizer.tokenize(data.gsub(/[^a-z \t'_\-\n.,+]/i, '')).map(&:downcase)
205
+ filtered = @stopwords_filter.filter(tokenized)
206
+ stemmed = filtered.map(&:stem).select{|s| not s.empty?}.map(&:to_sym)
207
+
208
+ return stemmed
209
+ end
210
+ end
211
+ end
212
+ end
213
+
214
+ Jekyll::Hooks.register :posts, :pre_render do |post|
215
+ Amadeusz::Jekyll::RelatedPosts.instance.add_post(post)
216
+ end
217
+
218
+ Jekyll::Hooks.register :site, :post_write do |site|
219
+ Amadeusz::Jekyll::RelatedPosts.instance.build! site
220
+ end
@@ -0,0 +1,12 @@
1
+ {% if related_posts != empty %}
2
+ <div id="related-posts">
3
+ <h3>Related posts</h3>
4
+ <ul>
5
+ {% for p in related_posts %}
6
+ <li>
7
+ <a href="{{ p.url }}" data-score="{{ p.score }}">{{ p.title }}</a>
8
+ </li>
9
+ {% endfor %}
10
+ </ul>
11
+ </div>
12
+ {% endif %}
metadata ADDED
@@ -0,0 +1,188 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jekyll-related-posts
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Amadeusz Juskowiak
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-11-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: jekyll
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: liquid
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: tokenizer
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: stopwords-filter
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.3'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: fast-stemmer
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pqueue
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.1'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.1'
111
+ - !ruby/object:Gem::Dependency
112
+ name: nmatrix
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.2'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.2'
125
+ - !ruby/object:Gem::Dependency
126
+ name: nmatrix-lapacke
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.2'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.2'
139
+ description: |-
140
+ Proper related posts plugin for Jekyll - uses document correlation matrix on TF-IDF (optionally with Latent Semantic Indexing).
141
+
142
+ Each document is tokenized and stemmed, every word found is treated as keyword for analysis (except for some stop words).
143
+
144
+ TF-IDF matrix for the whole site is calculated (including extra provided weights), then if given accuraccy is lower than 1.0, LSI algorithm is used to compute new simplified vector space. Document correlation matrix is created using dot product of the matrix and its transpose.
145
+
146
+ For each of the post' related documents are inserted into priority queue (sorted by score from document correlation matrix), assuming the score is greater than minimal required score. Selected few bests related posts are retrieven from the queue.
147
+
148
+ Liquid template for each post is rendered and <related-posts /> is replaced with the outcomes of algorithm.
149
+ email:
150
+ - juskowiak@amadeusz.me
151
+ executables: []
152
+ extensions: []
153
+ extra_rdoc_files: []
154
+ files:
155
+ - ".gitignore"
156
+ - Gemfile
157
+ - LICENSE.txt
158
+ - README.md
159
+ - jekyll-related-posts.gemspec
160
+ - lib/_config.yml
161
+ - lib/jekyll-related-posts.rb
162
+ - lib/related.html
163
+ homepage: https://github.com/alfanick/jekyll-related-posts
164
+ licenses:
165
+ - MIT
166
+ metadata: {}
167
+ post_install_message:
168
+ rdoc_options: []
169
+ require_paths:
170
+ - lib
171
+ required_ruby_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ requirements: []
182
+ rubyforge_project:
183
+ rubygems_version: 2.2.2
184
+ signing_key:
185
+ specification_version: 4
186
+ summary: Proper related posts plugin for Jekyll - uses document correlation matrix
187
+ on TF-IDF (optionally with Latent Semantic Indexing).
188
+ test_files: []